Skip to content

Guidelines

Dos and Don'ts

ts
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { 
ComputedRef
,
ExtractPropTypes
,
Ref
,
ToRef
} from 'vue';
import {
computed
,
reactive
,
ref
,
shallowRef
,
toRef
} from 'vue';
import type {
Ref
as
DemiRef
} from 'vue-demi';
/** Types */ /** * IVue core reactive instance type with an extended .toRefs() method added. */ export type
IVue
<
T
extends
AnyClass
> =
InstanceType
<
T
> & {
toRefs
:
IVueToRefsFn
<
T
>;
}; /** * Type definition for `.toRefs()` method converts reactive class properties to composable .value properties. * But if props = true OR unwrap = true is specified, the refs will be unwrapped refs to be able to be merged with the the root class properties without losing reactivity. */ interface
IVueToRefsFn
<
T
extends
AnyClass
> {
<
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[]):
Pick
<
IVueRefs
<
InstanceType
<
T
>>,
P
>; <
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[],
unwrap
: false):
Pick
<
IVueRefs
<
InstanceType
<
T
>>,
P
>; <
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[],
unwrap
: true):
Pick
<
InstanceType
<
T
>,
P
>; (
props
: true):
InstanceType
<
T
>;
(
props
: false):
IVueRefs
<
InstanceType
<
T
>>;
():
IVueRefs
<
InstanceType
<
T
>>;
} /** * Converts a class properties to a composable .value Refs using ToRef vue type, * also knows NOT to convert functions to .value Refs but to leave them as is. */ export type
IVueRefs
<
T
> = {
[
K
in keyof
T
]:
T
[
K
] extends Function ?
T
[
K
] :
ToRef
<
T
[
K
]>;
}; /** * Unwraps Ref Recursively, helps resolve fully the bare value types of any type T * because sometimes the Refs are returned as they are with `.value` from computeds, * thus inferring nested ComputedRef<ComputedRef<ComputedRef<Ref>>> types, which * are difficult to fully resolve to bare values without this utility. * Also support Vue 3 Demi for vue-use library support. */ type
UnwrapRefRecursively
<
T
= any> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
['value']>
:
T
;
/** * Helper type for Use type, unwraps any type of Vue 3 composable return object down to its bare types. */ type
UnwrapComposableRefs
<
T
> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
>
: { [
K
in keyof
T
]:
T
[
K
] extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
[
K
]>
:
T
[
K
];
}; /** * Fully unwraps to bare value types any Vue 3 composable return definition type. */ export type
Use
<
T
= any> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
>
:
UnwrapComposableRefs
<
T
extends
AnyFn
?
ReturnType
<
T
> :
T
>;
/** * Extracts object defined emit types by converting them to a plain interface. */ export type
ExtractEmitTypes
<
T
extends
Record
<string, any>> =
UnionToIntersection
<
RecordToUnion
<{
[
K
in keyof
T
]: (
evt
:
K
, ...
args
:
Parameters
<
T
[
K
]>) => void;
}> >; /** * Extract properties as all assigned properties because they have defaults. */ export type
ExtractPropDefaultTypes
<
O
> = {
[
K
in keyof
O
]:
ValueOf
<
ExtractPropTypes
<
O
>,
K
>;
}; /** * Extend slots interface T with prefixed 'before--' & 'after--' slots to create fully extensible flexible slots. */ export type
ExtendSlots
<
T
> =
PrefixKeys
<
T
, 'before--'> &
T
&
PrefixKeys
<
T
, 'after--'>;
/** Helper Types. */ /** * Any JavaScript function of any type. */ export type
AnyFn
= (...
args
: any[]) => any;
/** * Any JavaScript class of any type. */ export type
AnyClass
= new (...
args
: any[]) => any;
/** * Accessors map type. */ type
Accessors
=
Map
<string, PropertyDescriptor>;
/** * Computeds hash map type. */ type
Computeds
=
Record
<string,
ComputedRef
>;
/** * Infer paramaters of a constructor function of a Class. */ export type
InferredArgs
<
T
> =
T
extends { new (...
args
: infer
P
): any }
?
P
: never[]; /** * Prefix keys of an interface T with a prefix P. */ export type
PrefixKeys
<
T
,
P
extends string | undefined = undefined> = {
[
K
in
Extract
<keyof
T
, string> as
P
extends string ? `${
P
}${
K
}` :
K
]:
T
[
K
];
}; /** * Get Interface's property's function arguments Parmeters<F> */ export type
IFnParameters
<
T
extends
Record
<any, any>,
K
extends keyof
T
> =
Parameters
<
Required
<
Pick
<
T
,
K
>>[
K
]>;
/** * Get Interface's [T] property's [P] function arguments Parmeters<F> parameter by key [K] */ export type
IFnParameter
<
T
extends
Record
<any, any>,
P
extends keyof
T
,
K
extends number
> =
FnParameter
<
ValueOf
<
T
,
P
>,
K
>;
/** * Get function arguments Parmeters<F> parameter by key K */ export type
FnParameter
<
F
extends
AnyFn
,
K
extends number> =
Parameters
<
F
>[
K
];
/** * Convert Union Type to Intersection Type. */ export type
UnionToIntersection
<
U
> = (
U
extends any ? (
k
:
U
) => void : never
) extends (
k
: infer
I
) => void
?
I
: never; /** * Convert Record to Union Type. */ export type
RecordToUnion
<
T
extends
Record
<string, any>> =
T
[keyof
T
];
/** * Gets object T property by key [K]. */ export type
ValueOf
<
T
extends
Record
<any, any>,
K
> =
T
[
K
];
/** Types End. */ /** * Stores accessors for each class prototype processed by * @see {getAllClassAccessors}. Uses WeakMap to allow garbage collection * of unused class accessors, ensuring memory efficiency in long-running applications. */ export let
accessorsMap
= new
WeakMap
<object,
Accessors
>();
/** * Get accessors of an entire class prototype ancestors chain as a Map. * Completely emulates JavaScript class inheritance chain for getters and setters. * * @param className * @return {Accessors} */ export function
getAllClassAccessors
(
className
:
AnyClass
):
Accessors
{
if (
accessorsMap
.
has
(
className
)) {
return
accessorsMap
.
get
(
className
) as
Accessors
;
} const
savedAccessors
:
Accessors
= new
Map
();
let
prototype
=
className
.
prototype
;
while (
prototype
&&
prototype
!==
Object
.
prototype
) {
const
accessors
=
Object
.
getOwnPropertyDescriptors
(
prototype
);
for (const [
propertyName
,
descriptor
] of
Object
.
entries
(
accessors
)) {
const
isAccessor
=
descriptor
.
get
||
descriptor
.
set
;
if (!
isAccessor
) continue;
// Only add if it hasn't been defined yet (i.e. subclass overrides win) if (!
savedAccessors
.
has
(
propertyName
)) {
savedAccessors
.
set
(
propertyName
, {
get
:
descriptor
.
get
,
set
:
descriptor
.
set
,
enumerable
:
descriptor
.
enumerable
,
configurable
:
descriptor
.
configurable
,
}); } }
prototype
=
Object
.
getPrototypeOf
(
prototype
);
}
accessorsMap
.
set
(
className
,
savedAccessors
);
return
savedAccessors
;
} /** * Infinite Vue (ivue) class reactive initializer. * * Converts class instance to a reactive object, * where accessors are converted to computeds. * * You can turn off computed behaviour by adding static * ivue object and setting the getter props to false. * class ClassName { * static ivue = { * getter: false * } * // .getter -> will be a standard JS non-computed getter * get getter () { return 'hello world'; } * } * * @param className Any Class * @param args Class constructor arguments that you would pass to a `new AnyClass(args...)` * @returns {IVue<T>} */ export function
ivue
<
T
extends
AnyClass
>(
className
:
T
,
...
args
:
InferredArgs
<
T
>
):
IVue
<
T
> {
const
accessors
:
Accessors
=
getAllClassAccessors
(
className
);
const
computeds
:
Computeds
| any =
accessors
?.
size
? {} : null;
/** Create a reactive instance of the class. */ const
vue
=
reactive
(new
className
(...
args
));
/** Setup accessors as computeds. */ for (const [
prop
,
descriptor
] of
accessors
) {
/* If prop exists on static getter className.ivue[prop] * We do not convert it to computed. Because sometimes * we want a normal getter. */ if ((
className
as any)?.ivue?.[
prop
] === false) continue;
/** Convert descriptor to computed. */
Object
.
defineProperty
(
vue
,
prop
, {
get
:
descriptor
.
get
? () =>
prop
in
computeds
? /** Get the existing computed, because we are in reactive scope, .value will auto unwrap itself. */
computeds
[
prop
]
: /** Create the computed and return it, because we are in reactive scope, .value will auto unwrap itself. */ (
computeds
[
prop
] =
computed
({
get
:
descriptor
.
get
?.
bind
(
vue
) as any,
set
:
descriptor
.
set
?.
bind
(
vue
),
} as any)) :
undefined
,
set
:
descriptor
.
set
?.
bind
(
vue
),
enumerable
: false,
}); }
Object
.
defineProperty
(
vue
, 'toRefs', {
get
: () =>
ivueToRefs
(
vue
,
accessors
as
Accessors
,
computeds
),
enumerable
: false,
}); /** Run ivue .init() initializer method, if it exists in the class. */ if ('init' in
vue
)
vue
.init();
return
vue
;
} /** * Stores properties for each class prototype processed by * @see {getAllClassProperties}. Uses WeakMap to allow garbage collection * of unused class properties, ensuring memory efficiency in long-running applications. */ export let
propsMap
:
WeakMap
<object,
Set
<string>> = new
WeakMap
();
/** * Get properties of an entire class prototype ancestors chain as a Map. */ export function
getAllClassProperties
(
obj
: object):
Set
<string> {
/* Retrieve props from cache. */ if (
propsMap
.
has
(
obj
.
constructor
)) {
return
propsMap
.
get
(
obj
.
constructor
) as
Set
<string>;
} const
originalConstructor
=
obj
.
constructor
;
const
allProps
:
Set
<string> = new
Set
();
do {
Object
.
getOwnPropertyNames
(
obj
).
forEach
((
prop
) => {
/* 'caller', 'callee', 'arguments', 'constructor' are * special object properties, so should be skipped. */ if (!['caller', 'callee', 'arguments', 'constructor'].
includes
(
prop
)) {
allProps
.
add
(
prop
);
} });
obj
=
Object
.
getPrototypeOf
(
obj
);
} while (
obj
.
constructor
!==
Object
);
/** Save props in the props map. */
propsMap
.
set
(
originalConstructor
,
allProps
);
return
allProps
;
} /** * `iref()` is an alias for Vue ref() function but returns an unwrapped type without the .value * `iref()` does not alter the behavior of ref(), but simply transforms the type to an unwrapped raw value. * @param val T * @returns {T} */ export const
iref
=
ref
as <
T
= any>(
value
?:
T
) =>
T
;
/** * `ishallowRef()` is an alias for Vue shallowRef() function but returns an unwrapped type without the .value * `ishallowRef()` does not alter the behavior of shallowRef(), but simply transforms the type to an unwrapped raw value. * @param val T * @returns {T} */ export const
ishallowRef
=
shallowRef
as <
T
= any>(
value
?:
T
) =>
T
;
/** * Three modes of operation: * 1. `iuse(useComposable(arg, arg2, arg3, ...))` converts the return types of a Composable / Ref to pure raw unwrapped type definition. * Returns for all properties of an object an unwrapped raw type definition, * unwraps direct Refs & ComputedRefs as well down to their raw types. * * 2. `iuse(useComposable, arg, arg2, arg3, ...)` for cleaner syntax for #1, it does exactly the same thing but * here the TypeScript inference works for composable function arguments to assist you with intellisence, * like they work for constructor arguments in the cause of `ivue()` core function, * making the API cleaner to look at and make it compatible with how this function operates with classes, see #3. * * 3. `iuse(AnyClass, ...args)` If AnyClass is supplied and that class's arguments into `iuse(AnyClass, ...args)`, * it returns an 'ivue(AnyClass, ...args).toRefs()` object for all properties but casts * their types as raw (no-Ref) types to fit with reactive() structure of the ivue class context. */ export function
iuse
<
T
extends
AnyClass
|
AnyFn
| Object | any>(
classFunctionObject
?:
T
,
...
args
:
T
extends
AnyClass
?
InferredArgs
<
T
>
:
T
extends
AnyFn
?
Parameters
<
T
extends (...
args
: any[]) => any ?
T
: any>
: any ):
T
extends
AnyClass
?
InstanceType
<
T
> :
Use
<
T
> {
return typeof
classFunctionObject
=== 'function'
?
isClass
(
classFunctionObject
)
? /** Run IVUE but return full Refs, yet property 'true' makes `.toRefs(true)` cast the type to the unwrapped raw type definition instead of a `.value` Ref. */
ivue
(
classFunctionObject
as
T
extends
AnyClass
?
T
: any,
...(
args
as
InferredArgs
<
T
extends
AnyClass
?
T
: any>)
).toRefs(true) : /** Run Vue 3 Standard Composable but also unwrap it to bare raw types. */ (
classFunctionObject
as
AnyFn
)(
...(
args
as
Parameters
<
T
extends
AnyFn
?
AnyFn
: any>)
) : (
classFunctionObject
as unknown as
T
extends
AnyClass
?
InstanceType
<
T
>
:
Use
<
T
>) /** Unwrap any other Object or any type down to its bare types. */;
} /** * Convert reactive ivue class to Vue 3 refs. * * @param vue @see IVue * @param accessors @see Accessors * @param computeds @see Computeds * @returns {ExtendWithToRefs<T>['toRefs']} */ function
ivueToRefs
<
T
extends
AnyClass
>(
vue
:
IVue
<
T
>,
accessors
:
Accessors
,
computeds
:
Computeds
):
IVueToRefsFn
<
T
> {
return function (
props
?: (keyof
InstanceType
<
T
>)[] | boolean,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
unwrap
?: boolean /** This property helps with TypeScript function definition overloading of return types and is not being used inside the function itself. */
): any /** @see {ReturnType<IVueToRefsFn<T>>} */ { /** Resulting refs store. */ const
result
:
Record
<string | number | symbol, any> = {};
/** Output specific props only, if props are specified. */ if (
Array
.
isArray
(
props
)) {
for (let
i
= 0;
i
<
props
.
length
;
i
++) {
const
prop
=
props
[
i
] as any;
/** Handle accessors. */ if (
accessors
.
has
(
prop
)) {
if (
prop
in
computeds
) {
/** Return vue computed with .value from computeds store. */
result
[
prop
] =
computeds
[
prop
];
} else { /** Initialize & store vue computed. */ const
descriptor
=
accessors
.
get
(
prop
);
result
[
prop
] =
computeds
[
prop
] =
computed
({
get
:
descriptor
?.
get
?.
bind
(
vue
) as any,
set
:
descriptor
?.
set
?.
bind
(
vue
),
} as any); } } else { /** Handle methods. */ if (typeof
vue
[
prop
] === 'function') {
/** Bind method to vue, makes method destructuring point to right instance. */
result
[
prop
] =
vue
[
prop
].bind(
vue
);
} else { /** Convert simple reactive prop to a Ref. */
result
[
prop
] =
toRef
(
vue
,
prop
);
} } } } else { /** Convert all props to refs and leave functions as is. */ let
allProps
: null |
Set
<string> = new
Set
(
getAllClassProperties
(
vue
));
/** Convert accessors (non enumerable by default in JS). */
accessors
.
forEach
((
descriptor
,
prop
) => {
if (
prop
in
computeds
) {
/** Return vue computed with .value from computeds store. */
result
[
prop
] =
computeds
[
prop
];
} else { /** Initialize vue computed ref & store it in result. */
result
[
prop
] =
computeds
[
prop
] =
computed
({
get
:
descriptor
.
get
?.
bind
(
vue
) as any,
set
:
descriptor
.
set
?.
bind
(
vue
),
} as any); } /** Delete descriptor from props as it was already processed. */
allProps
?.
delete
(
prop
as string);
});
allProps
.
forEach
((
prop
) => {
if (typeof
vue
[
prop
] === 'function') {
/** Bind method to vue, makes method destructuring point to right instance. */
result
[
prop
] =
vue
[
prop
].bind(
vue
);
} else { /** Convert simple reactive prop to a Ref. */
result
[
prop
] =
toRef
(
vue
,
prop
);
} }); /** Memory optimization. */
allProps
= null;
} return
result
as any;
}; } /** * Vue props interface in defineComponent() style. */ export type
VuePropsObject
=
Record
<
string, {
type
: any;
default
?: any;
required
?: boolean }
>; /** * Vue Props with default properties declared as existing and having values. */ export type
VuePropsWithDefaults
<
T
extends
VuePropsObject
> = {
[
K
in keyof
T
]: {
type
:
T
[
K
]['type'];
default
:
T
[
K
]['default'];
required
?: boolean;
}; }; /** * Determines if the value is a JavaScript Class. * Note that class is a class function in JavaScript. * * @param val Any value * @returns boolean If it's a JavaScript Class returns true */ export function
isClass
(
val
: any): boolean {
if (typeof
val
!== 'function') return false; // Not a function, so not a class function either
if (!
val
.prototype) return false; // Arrow function, so not a class
// Finally -> distinguish between a normal function and a class function if (
Object
.
getOwnPropertyDescriptor
(
val
, 'prototype')?.
writable
) {
// Has writable prototype return false; // Normal function } else { return true; // Class -> Not a function } } /** * Creates props with defaults in defineComponent() style. * * Merge defaults regular object with Vue types object * declared in defineComponent() style. * * This is made so that the defaults can be declared "as they are" * without requiring objects to be function callbacks returning an object. * * // You don't need to wrap objects in () => ({ nest: { nest :{} } }) * // You can just delcare them normally. * const defaults = { * nest: { * nest * } * } * * This function will create the Vue expected callbacks for Objects, Arrays & Classes * but leave primitive properties and functions intact so that * the final object is fully defineComponent() style compatible. * * @param defaults Regular object of default key -> values * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default * @returns Props declared in defineComponent() style with all properties having default property declared. */ export function
propsWithDefaults
<
T
extends
VuePropsObject
>(
defaults
:
Record
<string, any>,
typedProps
:
T
):
VuePropsWithDefaults
<
T
> {
for (const
prop
in
typedProps
) {
if (typeof
defaults
?.[
prop
] === 'object' &&
defaults
?.[
prop
] !== null) {
/** Handle Arrays & Objects -> wrap them with an arrow function. */
typedProps
[
prop
].
default
= () =>
defaults
?.[
prop
];
} else { if (
isClass
(
defaults
?.[
prop
])) {
/** Handle JavaScript Classes -> wrap them with an arrow function */
typedProps
[
prop
].
default
= () =>
defaults
?.[
prop
];
} else { /** Handle JavaScript Function And All primitive properties -> output directly */
typedProps
[
prop
].
default
=
defaults
?.[
prop
];
} } } return
typedProps
as
VuePropsWithDefaults
<
T
>;
} /** * Clears a specific class's accessors from the cache or resets the entire cache. * Useful for SSR or HMR to ensure immediate memory release. */ export function
clearAccessorsMap
(
className
?:
AnyClass
) {
if (
className
) {
accessorsMap
.
delete
(
className
);
} else {
accessorsMap
= new
WeakMap
();
} } /** * Clears a specific class's properties from the cache or resets the entire cache. * Useful for SSR or HMR to ensure immediate memory release. */ export function
clearPropsMap
(
classConstructor
?: object) {
if (
classConstructor
) {
propsMap
.
delete
(
classConstructor
);
} else {
propsMap
= new
WeakMap
();
} } /** Necessary ivue.ts to be treated as a module. */ export {};
ts
import { 
ref
,
onMounted
,
onUnmounted
} from 'vue';
export function
useMouse
() {
const
x
=
ref
(0);
const
y
=
ref
(0);
const
sourceType
=
ref
<'mouse' | 'touch'>('mouse');
function
updateMouse
(
event
: MouseEvent) {
sourceType
.
value
= 'mouse';
x
.
value
=
event
.
clientX
;
y
.
value
=
event
.
clientY
;
} function
updateTouch
(
event
: TouchEvent) {
sourceType
.
value
= 'touch';
if (
event
.
touches
.
length
> 0) {
const
touch
=
event
.
touches
[0];
x
.
value
=
touch
.
clientX
;
y
.
value
=
touch
.
clientY
;
} }
onMounted
(() => {
window
.
addEventListener
('mousemove',
updateMouse
);
window
.
addEventListener
('touchstart',
updateTouch
);
window
.
addEventListener
('touchmove',
updateTouch
);
});
onUnmounted
(() => {
window
.
removeEventListener
('mousemove',
updateMouse
);
window
.
removeEventListener
('touchstart',
updateTouch
);
window
.
removeEventListener
('touchmove',
updateTouch
);
}); return {
x
,
y
,
sourceType
,
}; }
ts
import { 
defineProps
,
defineEmits
,
onMounted
,
watch
} from 'vue';
import { type
Use
,
ivue
,
iref
,
iuse
} from 'ivue';
import {
useMouse
} from './useMouse';
// Properly declared unwrapped composable. type
UseMouse
=
Use
<typeof
useMouse
>;
interface CounterProps {
initialCount
: number;
} interface CounterEmits { (
e
: 'increment',
count
: number): void;
} const
props
=
defineProps
<CounterProps>();
const
emit
=
defineEmits
<CounterEmits>();
/** * Example of a properly defined ivue class. */ class
Counter
{
// Properly declared unwrapped composable.
mouse
:
UseMouse
;
// Properly declared DOM Ref.
spanElementRef
=
iref
<HTMLElement | null>(null);
constructor(public
props
: CounterProps, public
emit
: CounterEmits) {
// Properly declared auto-unwrapped composable. this.
mouse
=
iuse
(
useMouse
);
} /** * Properly declared ivue init() function * used to initialize Vue lifecycle hooks. */
init
() {
// Properly set lifecycle hook.
onMounted
(() => {
this.
count
= 4;
}); // Properly set watch function.
watch
(
() => this.
count
,
(
newCount
: number) => {
if (
newCount
=== 5) {
alert
('You reached the count of ' +
newCount
+ '!');
} } ); }
count
=
iref
(0); // Properly declared Ref with auto inferred type -> number
timesClicked
=
iref
(0); // Properly declared Ref with auto inferred type -> number
// Properly declared function (not arrow function).
increment
() {
this.
count
++;
} // Properly declared function (not arrow function).
click
() {
this.
increment
();
this.
timesClicked
++;
} // Properly declared computed getter. get
doubleCount
() {
return this.
count
* 2;
} } // Properly initialized ivue class. const
counter
=
ivue
(
Counter
,
props
,
emit
);
counter
.
count
;
ts
import { 
ref
,
onMounted
,
onUnmounted
} from 'vue';
export function
useMouse
() {
const
x
=
ref
(0);
const
y
=
ref
(0);
const
sourceType
=
ref
<'mouse' | 'touch'>('mouse');
function
updateMouse
(
event
: MouseEvent) {
sourceType
.
value
= 'mouse';
x
.
value
=
event
.
clientX
;
y
.
value
=
event
.
clientY
;
} function
updateTouch
(
event
: TouchEvent) {
sourceType
.
value
= 'touch';
if (
event
.
touches
.
length
> 0) {
const
touch
=
event
.
touches
[0];
x
.
value
=
touch
.
clientX
;
y
.
value
=
touch
.
clientY
;
} }
onMounted
(() => {
window
.
addEventListener
('mousemove',
updateMouse
);
window
.
addEventListener
('touchstart',
updateTouch
);
window
.
addEventListener
('touchmove',
updateTouch
);
});
onUnmounted
(() => {
window
.
removeEventListener
('mousemove',
updateMouse
);
window
.
removeEventListener
('touchstart',
updateTouch
);
window
.
removeEventListener
('touchmove',
updateTouch
);
}); return {
x
,
y
,
sourceType
,
}; }
ts
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { 
ComputedRef
,
ExtractPropTypes
,
Ref
,
ToRef
} from 'vue';
import {
computed
,
reactive
,
ref
,
shallowRef
,
toRef
} from 'vue';
import type {
Ref
as
DemiRef
} from 'vue-demi';
/** Types */ /** * IVue core reactive instance type with an extended .toRefs() method added. */ export type
IVue
<
T
extends
AnyClass
> =
InstanceType
<
T
> & {
toRefs
:
IVueToRefsFn
<
T
>;
}; /** * Type definition for `.toRefs()` method converts reactive class properties to composable .value properties. * But if props = true OR unwrap = true is specified, the refs will be unwrapped refs to be able to be merged with the the root class properties without losing reactivity. */ interface
IVueToRefsFn
<
T
extends
AnyClass
> {
<
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[]):
Pick
<
IVueRefs
<
InstanceType
<
T
>>,
P
>; <
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[],
unwrap
: false):
Pick
<
IVueRefs
<
InstanceType
<
T
>>,
P
>; <
P
extends keyof
InstanceType
<
T
>>(
props
:
P
[],
unwrap
: true):
Pick
<
InstanceType
<
T
>,
P
>; (
props
: true):
InstanceType
<
T
>;
(
props
: false):
IVueRefs
<
InstanceType
<
T
>>;
():
IVueRefs
<
InstanceType
<
T
>>;
} /** * Converts a class properties to a composable .value Refs using ToRef vue type, * also knows NOT to convert functions to .value Refs but to leave them as is. */ export type
IVueRefs
<
T
> = {
[
K
in keyof
T
]:
T
[
K
] extends Function ?
T
[
K
] :
ToRef
<
T
[
K
]>;
}; /** * Unwraps Ref Recursively, helps resolve fully the bare value types of any type T * because sometimes the Refs are returned as they are with `.value` from computeds, * thus inferring nested ComputedRef<ComputedRef<ComputedRef<Ref>>> types, which * are difficult to fully resolve to bare values without this utility. * Also support Vue 3 Demi for vue-use library support. */ type
UnwrapRefRecursively
<
T
= any> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
['value']>
:
T
;
/** * Helper type for Use type, unwraps any type of Vue 3 composable return object down to its bare types. */ type
UnwrapComposableRefs
<
T
> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
>
: { [
K
in keyof
T
]:
T
[
K
] extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
[
K
]>
:
T
[
K
];
}; /** * Fully unwraps to bare value types any Vue 3 composable return definition type. */ export type
Use
<
T
= any> =
T
extends
Ref
|
DemiRef
?
UnwrapRefRecursively
<
T
>
:
UnwrapComposableRefs
<
T
extends
AnyFn
?
ReturnType
<
T
> :
T
>;
/** * Extracts object defined emit types by converting them to a plain interface. */ export type
ExtractEmitTypes
<
T
extends
Record
<string, any>> =
UnionToIntersection
<
RecordToUnion
<{
[
K
in keyof
T
]: (
evt
:
K
, ...
args
:
Parameters
<
T
[
K
]>) => void;
}> >; /** * Extract properties as all assigned properties because they have defaults. */ export type
ExtractPropDefaultTypes
<
O
> = {
[
K
in keyof
O
]:
ValueOf
<
ExtractPropTypes
<
O
>,
K
>;
}; /** * Extend slots interface T with prefixed 'before--' & 'after--' slots to create fully extensible flexible slots. */ export type
ExtendSlots
<
T
> =
PrefixKeys
<
T
, 'before--'> &
T
&
PrefixKeys
<
T
, 'after--'>;
/** Helper Types. */ /** * Any JavaScript function of any type. */ export type
AnyFn
= (...
args
: any[]) => any;
/** * Any JavaScript class of any type. */ export type
AnyClass
= new (...
args
: any[]) => any;
/** * Accessors map type. */ type
Accessors
=
Map
<string, PropertyDescriptor>;
/** * Computeds hash map type. */ type
Computeds
=
Record
<string,
ComputedRef
>;
/** * Infer paramaters of a constructor function of a Class. */ export type
InferredArgs
<
T
> =
T
extends { new (...
args
: infer
P
): any }
?
P
: never[]; /** * Prefix keys of an interface T with a prefix P. */ export type
PrefixKeys
<
T
,
P
extends string | undefined = undefined> = {
[
K
in
Extract
<keyof
T
, string> as
P
extends string ? `${
P
}${
K
}` :
K
]:
T
[
K
];
}; /** * Get Interface's property's function arguments Parmeters<F> */ export type
IFnParameters
<
T
extends
Record
<any, any>,
K
extends keyof
T
> =
Parameters
<
Required
<
Pick
<
T
,
K
>>[
K
]>;
/** * Get Interface's [T] property's [P] function arguments Parmeters<F> parameter by key [K] */ export type
IFnParameter
<
T
extends
Record
<any, any>,
P
extends keyof
T
,
K
extends number
> =
FnParameter
<
ValueOf
<
T
,
P
>,
K
>;
/** * Get function arguments Parmeters<F> parameter by key K */ export type
FnParameter
<
F
extends
AnyFn
,
K
extends number> =
Parameters
<
F
>[
K
];
/** * Convert Union Type to Intersection Type. */ export type
UnionToIntersection
<
U
> = (
U
extends any ? (
k
:
U
) => void : never
) extends (
k
: infer
I
) => void
?
I
: never; /** * Convert Record to Union Type. */ export type
RecordToUnion
<
T
extends
Record
<string, any>> =
T
[keyof
T
];
/** * Gets object T property by key [K]. */ export type
ValueOf
<
T
extends
Record
<any, any>,
K
> =
T
[
K
];
/** Types End. */ /** * Stores accessors for each class prototype processed by * @see {getAllClassAccessors}. Uses WeakMap to allow garbage collection * of unused class accessors, ensuring memory efficiency in long-running applications. */ export let
accessorsMap
= new
WeakMap
<object,
Accessors
>();
/** * Get accessors of an entire class prototype ancestors chain as a Map. * Completely emulates JavaScript class inheritance chain for getters and setters. * * @param className * @return {Accessors} */ export function
getAllClassAccessors
(
className
:
AnyClass
):
Accessors
{
if (
accessorsMap
.
has
(
className
)) {
return
accessorsMap
.
get
(
className
) as
Accessors
;
} const
savedAccessors
:
Accessors
= new
Map
();
let
prototype
=
className
.
prototype
;
while (
prototype
&&
prototype
!==
Object
.
prototype
) {
const
accessors
=
Object
.
getOwnPropertyDescriptors
(
prototype
);
for (const [
propertyName
,
descriptor
] of
Object
.
entries
(
accessors
)) {
const
isAccessor
=
descriptor
.
get
||
descriptor
.
set
;
if (!
isAccessor
) continue;
// Only add if it hasn't been defined yet (i.e. subclass overrides win) if (!
savedAccessors
.
has
(
propertyName
)) {
savedAccessors
.
set
(
propertyName
, {
get
:
descriptor
.
get
,
set
:
descriptor
.
set
,
enumerable
:
descriptor
.
enumerable
,
configurable
:
descriptor
.
configurable
,
}); } }
prototype
=
Object
.
getPrototypeOf
(
prototype
);
}
accessorsMap
.
set
(
className
,
savedAccessors
);
return
savedAccessors
;
} /** * Infinite Vue (ivue) class reactive initializer. * * Converts class instance to a reactive object, * where accessors are converted to computeds. * * You can turn off computed behaviour by adding static * ivue object and setting the getter props to false. * class ClassName { * static ivue = { * getter: false * } * // .getter -> will be a standard JS non-computed getter * get getter () { return 'hello world'; } * } * * @param className Any Class * @param args Class constructor arguments that you would pass to a `new AnyClass(args...)` * @returns {IVue<T>} */ export function
ivue
<
T
extends
AnyClass
>(
className
:
T
,
...
args
:
InferredArgs
<
T
>
):
IVue
<
T
> {
const
accessors
:
Accessors
=
getAllClassAccessors
(
className
);
const
computeds
:
Computeds
| any =
accessors
?.
size
? {} : null;
/** Create a reactive instance of the class. */ const
vue
=
reactive
(new
className
(...
args
));
/** Setup accessors as computeds. */ for (const [
prop
,
descriptor
] of
accessors
) {
/* If prop exists on static getter className.ivue[prop] * We do not convert it to computed. Because sometimes * we want a normal getter. */ if ((
className
as any)?.ivue?.[
prop
] === false) continue;
/** Convert descriptor to computed. */
Object
.
defineProperty
(
vue
,
prop
, {
get
:
descriptor
.
get
? () =>
prop
in
computeds
? /** Get the existing computed, because we are in reactive scope, .value will auto unwrap itself. */
computeds
[
prop
]
: /** Create the computed and return it, because we are in reactive scope, .value will auto unwrap itself. */ (
computeds
[
prop
] =
computed
({
get
:
descriptor
.
get
?.
bind
(
vue
) as any,
set
:
descriptor
.
set
?.
bind
(
vue
),
} as any)) :
undefined
,
set
:
descriptor
.
set
?.
bind
(
vue
),
enumerable
: false,
}); }
Object
.
defineProperty
(
vue
, 'toRefs', {
get
: () =>
ivueToRefs
(
vue
,
accessors
as
Accessors
,
computeds
),
enumerable
: false,
}); /** Run ivue .init() initializer method, if it exists in the class. */ if ('init' in
vue
)
vue
.init();
return
vue
;
} /** * Stores properties for each class prototype processed by * @see {getAllClassProperties}. Uses WeakMap to allow garbage collection * of unused class properties, ensuring memory efficiency in long-running applications. */ export let
propsMap
:
WeakMap
<object,
Set
<string>> = new
WeakMap
();
/** * Get properties of an entire class prototype ancestors chain as a Map. */ export function
getAllClassProperties
(
obj
: object):
Set
<string> {
/* Retrieve props from cache. */ if (
propsMap
.
has
(
obj
.
constructor
)) {
return
propsMap
.
get
(
obj
.
constructor
) as
Set
<string>;
} const
originalConstructor
=
obj
.
constructor
;
const
allProps
:
Set
<string> = new
Set
();
do {
Object
.
getOwnPropertyNames
(
obj
).
forEach
((
prop
) => {
/* 'caller', 'callee', 'arguments', 'constructor' are * special object properties, so should be skipped. */ if (!['caller', 'callee', 'arguments', 'constructor'].
includes
(
prop
)) {
allProps
.
add
(
prop
);
} });
obj
=
Object
.
getPrototypeOf
(
obj
);
} while (
obj
.
constructor
!==
Object
);
/** Save props in the props map. */
propsMap
.
set
(
originalConstructor
,
allProps
);
return
allProps
;
} /** * `iref()` is an alias for Vue ref() function but returns an unwrapped type without the .value * `iref()` does not alter the behavior of ref(), but simply transforms the type to an unwrapped raw value. * @param val T * @returns {T} */ export const
iref
=
ref
as <
T
= any>(
value
?:
T
) =>
T
;
/** * `ishallowRef()` is an alias for Vue shallowRef() function but returns an unwrapped type without the .value * `ishallowRef()` does not alter the behavior of shallowRef(), but simply transforms the type to an unwrapped raw value. * @param val T * @returns {T} */ export const
ishallowRef
=
shallowRef
as <
T
= any>(
value
?:
T
) =>
T
;
/** * Three modes of operation: * 1. `iuse(useComposable(arg, arg2, arg3, ...))` converts the return types of a Composable / Ref to pure raw unwrapped type definition. * Returns for all properties of an object an unwrapped raw type definition, * unwraps direct Refs & ComputedRefs as well down to their raw types. * * 2. `iuse(useComposable, arg, arg2, arg3, ...)` for cleaner syntax for #1, it does exactly the same thing but * here the TypeScript inference works for composable function arguments to assist you with intellisence, * like they work for constructor arguments in the cause of `ivue()` core function, * making the API cleaner to look at and make it compatible with how this function operates with classes, see #3. * * 3. `iuse(AnyClass, ...args)` If AnyClass is supplied and that class's arguments into `iuse(AnyClass, ...args)`, * it returns an 'ivue(AnyClass, ...args).toRefs()` object for all properties but casts * their types as raw (no-Ref) types to fit with reactive() structure of the ivue class context. */ export function
iuse
<
T
extends
AnyClass
|
AnyFn
| Object | any>(
classFunctionObject
?:
T
,
...
args
:
T
extends
AnyClass
?
InferredArgs
<
T
>
:
T
extends
AnyFn
?
Parameters
<
T
extends (...
args
: any[]) => any ?
T
: any>
: any ):
T
extends
AnyClass
?
InstanceType
<
T
> :
Use
<
T
> {
return typeof
classFunctionObject
=== 'function'
?
isClass
(
classFunctionObject
)
? /** Run IVUE but return full Refs, yet property 'true' makes `.toRefs(true)` cast the type to the unwrapped raw type definition instead of a `.value` Ref. */
ivue
(
classFunctionObject
as
T
extends
AnyClass
?
T
: any,
...(
args
as
InferredArgs
<
T
extends
AnyClass
?
T
: any>)
).toRefs(true) : /** Run Vue 3 Standard Composable but also unwrap it to bare raw types. */ (
classFunctionObject
as
AnyFn
)(
...(
args
as
Parameters
<
T
extends
AnyFn
?
AnyFn
: any>)
) : (
classFunctionObject
as unknown as
T
extends
AnyClass
?
InstanceType
<
T
>
:
Use
<
T
>) /** Unwrap any other Object or any type down to its bare types. */;
} /** * Convert reactive ivue class to Vue 3 refs. * * @param vue @see IVue * @param accessors @see Accessors * @param computeds @see Computeds * @returns {ExtendWithToRefs<T>['toRefs']} */ function
ivueToRefs
<
T
extends
AnyClass
>(
vue
:
IVue
<
T
>,
accessors
:
Accessors
,
computeds
:
Computeds
):
IVueToRefsFn
<
T
> {
return function (
props
?: (keyof
InstanceType
<
T
>)[] | boolean,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
unwrap
?: boolean /** This property helps with TypeScript function definition overloading of return types and is not being used inside the function itself. */
): any /** @see {ReturnType<IVueToRefsFn<T>>} */ { /** Resulting refs store. */ const
result
:
Record
<string | number | symbol, any> = {};
/** Output specific props only, if props are specified. */ if (
Array
.
isArray
(
props
)) {
for (let
i
= 0;
i
<
props
.
length
;
i
++) {
const
prop
=
props
[
i
] as any;
/** Handle accessors. */ if (
accessors
.
has
(
prop
)) {
if (
prop
in
computeds
) {
/** Return vue computed with .value from computeds store. */
result
[
prop
] =
computeds
[
prop
];
} else { /** Initialize & store vue computed. */ const
descriptor
=
accessors
.
get
(
prop
);
result
[
prop
] =
computeds
[
prop
] =
computed
({
get
:
descriptor
?.
get
?.
bind
(
vue
) as any,
set
:
descriptor
?.
set
?.
bind
(
vue
),
} as any); } } else { /** Handle methods. */ if (typeof
vue
[
prop
] === 'function') {
/** Bind method to vue, makes method destructuring point to right instance. */
result
[
prop
] =
vue
[
prop
].bind(
vue
);
} else { /** Convert simple reactive prop to a Ref. */
result
[
prop
] =
toRef
(
vue
,
prop
);
} } } } else { /** Convert all props to refs and leave functions as is. */ let
allProps
: null |
Set
<string> = new
Set
(
getAllClassProperties
(
vue
));
/** Convert accessors (non enumerable by default in JS). */
accessors
.
forEach
((
descriptor
,
prop
) => {
if (
prop
in
computeds
) {
/** Return vue computed with .value from computeds store. */
result
[
prop
] =
computeds
[
prop
];
} else { /** Initialize vue computed ref & store it in result. */
result
[
prop
] =
computeds
[
prop
] =
computed
({
get
:
descriptor
.
get
?.
bind
(
vue
) as any,
set
:
descriptor
.
set
?.
bind
(
vue
),
} as any); } /** Delete descriptor from props as it was already processed. */
allProps
?.
delete
(
prop
as string);
});
allProps
.
forEach
((
prop
) => {
if (typeof
vue
[
prop
] === 'function') {
/** Bind method to vue, makes method destructuring point to right instance. */
result
[
prop
] =
vue
[
prop
].bind(
vue
);
} else { /** Convert simple reactive prop to a Ref. */
result
[
prop
] =
toRef
(
vue
,
prop
);
} }); /** Memory optimization. */
allProps
= null;
} return
result
as any;
}; } /** * Vue props interface in defineComponent() style. */ export type
VuePropsObject
=
Record
<
string, {
type
: any;
default
?: any;
required
?: boolean }
>; /** * Vue Props with default properties declared as existing and having values. */ export type
VuePropsWithDefaults
<
T
extends
VuePropsObject
> = {
[
K
in keyof
T
]: {
type
:
T
[
K
]['type'];
default
:
T
[
K
]['default'];
required
?: boolean;
}; }; /** * Determines if the value is a JavaScript Class. * Note that class is a class function in JavaScript. * * @param val Any value * @returns boolean If it's a JavaScript Class returns true */ export function
isClass
(
val
: any): boolean {
if (typeof
val
!== 'function') return false; // Not a function, so not a class function either
if (!
val
.prototype) return false; // Arrow function, so not a class
// Finally -> distinguish between a normal function and a class function if (
Object
.
getOwnPropertyDescriptor
(
val
, 'prototype')?.
writable
) {
// Has writable prototype return false; // Normal function } else { return true; // Class -> Not a function } } /** * Creates props with defaults in defineComponent() style. * * Merge defaults regular object with Vue types object * declared in defineComponent() style. * * This is made so that the defaults can be declared "as they are" * without requiring objects to be function callbacks returning an object. * * // You don't need to wrap objects in () => ({ nest: { nest :{} } }) * // You can just delcare them normally. * const defaults = { * nest: { * nest * } * } * * This function will create the Vue expected callbacks for Objects, Arrays & Classes * but leave primitive properties and functions intact so that * the final object is fully defineComponent() style compatible. * * @param defaults Regular object of default key -> values * @param typedProps Props declared in defineComponent() style with type and possibly required declared, but without default * @returns Props declared in defineComponent() style with all properties having default property declared. */ export function
propsWithDefaults
<
T
extends
VuePropsObject
>(
defaults
:
Record
<string, any>,
typedProps
:
T
):
VuePropsWithDefaults
<
T
> {
for (const
prop
in
typedProps
) {
if (typeof
defaults
?.[
prop
] === 'object' &&
defaults
?.[
prop
] !== null) {
/** Handle Arrays & Objects -> wrap them with an arrow function. */
typedProps
[
prop
].
default
= () =>
defaults
?.[
prop
];
} else { if (
isClass
(
defaults
?.[
prop
])) {
/** Handle JavaScript Classes -> wrap them with an arrow function */
typedProps
[
prop
].
default
= () =>
defaults
?.[
prop
];
} else { /** Handle JavaScript Function And All primitive properties -> output directly */
typedProps
[
prop
].
default
=
defaults
?.[
prop
];
} } } return
typedProps
as
VuePropsWithDefaults
<
T
>;
} /** * Clears a specific class's accessors from the cache or resets the entire cache. * Useful for SSR or HMR to ensure immediate memory release. */ export function
clearAccessorsMap
(
className
?:
AnyClass
) {
if (
className
) {
accessorsMap
.
delete
(
className
);
} else {
accessorsMap
= new
WeakMap
();
} } /** * Clears a specific class's properties from the cache or resets the entire cache. * Useful for SSR or HMR to ensure immediate memory release. */ export function
clearPropsMap
(
classConstructor
?: object) {
if (
classConstructor
) {
propsMap
.
delete
(
classConstructor
);
} else {
propsMap
= new
WeakMap
();
} } /** Necessary ivue.ts to be treated as a module. */ export {};

Use ref() for properties

ivue recommends all class properties to be defined as ref() to be able to interoperate with defineExpose(), if you simply pass reactive props which are not Refs through defineExpose(), they will lose reactivity. ref() refs just like computed refs get flattened into the reactive() object, so there is no need to worry about using .value. The ref() refs are necessary just internally for Vue 3 to know which refs to keep reactive.

Unwrap (de-Ref) the types

Next, we convert the types back to their normal types as if they have no reactivity at all, so Ref<number> is number in ivue, so rather than going in the direction of complexifying the types, we are going in the opposite direction towards simplification.

Use standard declaration syntax for functions

ivue recommends all class functions to be defined in plain full function style (not arrow functions), this allows all ivue classes to be extensible at any point. By using plain standard functions, it allows the developer to be able to override them at any time by simply extending the class.

Do not use arrow functions in class declarations

WARNING

Arrow functions break full extensibility of classes because they carry their own context at the point of declaration, so avoid using them inside of ivue classes.

constructor() vs .init()

Use constructor() to assign properties of the class and cast Refs to Unwrapped bare types.

Use .init() to declare reactive state functions like watch, watchEffect, and lifecycle hooks like onMounted, onBeforeMount etc, do assignments of reactive properties, since .init() already has access to reactive() state through this.


Inside the constructor() method you still have access to non-reactive state, because when constructor() is initialized, it does NOT yet have access to the reactive properties of the class, since it was not yet converted to reactive() by ivue, so if you use the properties like Refs or ComputedRefs inside constructor() you would have to use them with the .value.

As a general rule, there is no need to manipulate the values in the constructor(), use constructor only for assigning the properties and casting the types of those assigned properties to the unwrapped (de-Refed) final state of the resulting reactive() object.

Let's look at a constructor() vs .init() example:

vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ivue, iref, iuse } from 'ivue';

class Counter {
  constructor(public span?: HTMLElement) {
    // ❌ Do not do this in the constructor() because this.span 
    // still refers to this.span.value here:
    // onMounted(() => { 
    //   (this.span as HTMLElement /*❌*/).innerHTML = 'Initial span text!';
    // });
  }
  init() {
    // ✅ Do this inside init() because this.span (.value) is now
    // flattened & (this) is now reactive():
    onMounted(() => {
      (this.span as HTMLElement/*✅*/).innerHTML = 'Initial span text!';
    });
  }
  count = iref(0);
  increment() {
    this.count++;
    (this.span as HTMLElement).innerHTML = String(this.count + 1);
  }
}

const span = ref<HTMLElement>();

const counter = ivue(
  Counter,
  iuse(span)
);

defineExpose<Counter>(counter);
</script>
<template>
  <a href="javascript:void(0)" @click="() => counter.increment()">Increment</a><br />
  Count: {{ counter.count }} <br />
  <span ref="span"></span>
</template>
For this example we initialize the component like this:
vue
<template>
  <CounterExternalRefsDetailed />
</template>
Result
Increment
Count: 0

Unwrapping Refs

The key to working with ivue is understanding correctly the way Vue 3 does automatic Unwrapping of Refs when they are passed into the reactive() object. In that regard we are not relying on some magic ivue behavior but rather the default behavior of reactive() Vue 3 function.

To match that unwrapping behavior, our class needs to Unwrap (or de-Ref) the types of Composables, Refs, ComputedRefs, if they are being passed into the constructor, to get their raw basic types those Refs are pointing to, so that you can start operating in the ivue environment, where there is no need to worry about .value. This unwrapping should be mainly done inside the constructor() method.

Naming Conventions

To benefit from the full power of ivue, it is recommended to extract the classes into separate files. What has been an effective pattern is to name the classes and put them right beside components in the same folder that these classes are being used with. So if you have CounterComponent.vue component, it can have a class inside CounterComponentClass.ts, and you can store props, emits, and other runtime definitions inside CounterComponentProps.ts.





File Uploader