ivue.ts used in the examples below (click to expand)
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 {};
vue
<script setup lang="ts">
import { ref } from 'vue';
import { ivue, iref, iuse } from 'ivue';
class Counter {
constructor(public span?: HTMLElement) {}
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>
<button @click="() => counter.increment()">
Count: {{ counter.count }} + 1 = <span ref="span"></span>
</button>
</template>
vue
<script setup lang="ts">
import { ivue, iref } from 'ivue';
class Counter {
count = iref(0);
increment() {
this.count++;
}
}
function useCounter() {
/** Convert ivue Counter to a composable with .toRefs() */
return ivue(Counter).toRefs();
}
const { count, increment } = useCounter();
</script>
<template>
<button @click="() => increment()">
Count: {{ count }}
</button>
</template>
ts
import { computed, ref, type Ref } from 'vue';
import { useMouse } from '@vueuse/core';
type UseMouse = { x: Ref<number>; y: Ref<number> };
export function useCustomMouse(requiredProp: number) {
const { x, y }: UseMouse = useMouse();
const _sum = ref(0);
function sum() {
_sum.value = x.value + y.value + requiredProp;
}
const total = computed(() => {
return _sum.value;
});
return {
x,
y,
_sum,
sum,
total,
};
}
vue
<script setup lang="ts">
import { ivue, iuse, iref, type Use } from 'ivue';
import { useCustomMouse } from '@/components/usage/functions/useCustomMouse';
/**
* Use the ivue Utility Type: Use<typeof YourComposableFunctionName>
* to get he resulting unwrapped composable properties and functions.
*/
type UseCustomMouse = Use<typeof useCustomMouse>;
class Counter {
count = iref(0);
increment() {
this.count++;
}
/**
* 'x', 'y', 'sum', 'total' are Refs that will be unwrapped to their bare raw types and destructured into the class.
* Even though unwrapped (de-Refed), they will maintain their behavior as Refs and thus will maintain reactivity
* and at the same time get destructured into this class root level scope because
* Vue 3's `reactive()` Proxy will be able to resolve those Refs internally.
*/
x: UseCustomMouse['x']; // Unwrapped Ref<number> becomes -> number
y: UseCustomMouse['y']; // Unwrapped Ref<number> becomes -> number
sum: UseCustomMouse['sum']; // 'sum' method that will be destructured into this class on construct()
total: UseCustomMouse['total']; // 'total' computed Ref that will also be destructured into this class on construct()
constructor() {
({
x: this.x,
y: this.y,
sum: this.sum,
total: this.total,
} = iuse(useCustomMouse, 5));
}
}
const counter = ivue(Counter);
</script>
<template>
Count: {{ counter.count }} <br />
<a href="javascript:void(0)" @click="() => counter.increment()">
Increment
</a><br />
Mouse X: {{ counter.x }}, Y: {{ counter.y }}
<br />
Total (computed): {{ counter.total }}
<br />
<button class="button" @click="() => counter.sum()">
Click This Big Sum Button To Total X + Y
</button>
</template>