Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active January 18, 2024 21:38
Show Gist options
  • Save nberlette/b0f9d82c7399ed6237816c40ef11e887 to your computer and use it in GitHub Desktop.
Save nberlette/b0f9d82c7399ed6237816c40ef11e887 to your computer and use it in GitHub Desktop.
Chainable Stage 3 Decorators (experimental)
/*~----------------------------------------~*\
* CHAINABLE DECORATORS UTILITY CLASS *
* FOR TYPESCRIPT 5.0 *
*~----------------------------------------~*
* © 2023-2024 NICHOLAS BERLETTE • MIT *
\*~----------------------------------------~*/
// deno-lint-ignore-file no-explicit-any ban-types
import type {
IsomorphicDecorator,
DecoratorArgs,
Decorator,
DecoratorOrDecoratorFactory,
DecoratorFactory,
UnionToTuple,
Is,
} from "./helpers.ts";
import { has } from "./helpers.ts";
export type Chained<
T extends {},
E extends readonly (readonly [keyof T, ...unknown[]])[] = [],
> =
& Chainable<T extends Chainable<infer U> ? U : {}>
& (E[number][0] extends infer A extends readonly unknown[]
? { args: UnionToTuple<A> }
: unknown)
& {
readonly [K in Exclude<keyof T, E[number][0]>]: T[K] extends
(...args: infer A) => infer R ? A extends DecoratorArgs ?
& (<const Args extends readonly unknown[]>(
...args: Args
) => Chained<T, [...E, [K, ...Args]]>)
& Chained<T, [...E, [K]]>
: R extends Decorator ?
& (<const Args extends A>(
...args: Args
) => Chained<T, [...E, [K, ...Args]]>)
& Chained<T, [...E, [K]]>
: (() => Chained<T, [...E, [K]]>) & Chained<T, [...E, [K]]>
: never;
};
export interface Chainable<
T extends Record<string, DecoratorOrDecoratorFactory> = {},
> extends IsomorphicDecorator<unknown, unknown> {
<This, Value>(): IsomorphicDecorator<This, Value>;
constructor: (typeof Chainable) & {
new <const D extends T>(
decorators: D,
): Chained<
{
[
K in keyof D as D[K] extends Decorator ? K
: D[K] extends DecoratorFactory<any, any, any> ? K
: never
]: D[K] extends DecoratorFactory ? D[K]
: D[K] & (() => D[K]);
}
>;
};
}
export class Chainable<
const T extends Record<string, DecoratorOrDecoratorFactory> = {},
> extends Function {
/**
* Performs extensive checks to provide a bulletproof confirmation that a pair of
* arguments belong to a Stage 3 Decorator Function, immediately exiting if any
* of the checks fails to pass (for performance). Checks include:
* - IF length < 2 OR length > 2
* => FALSE
*
* -> target = args[0] , context = args[1]
*
* - IF typeof context !== "object"
* => FALSE
* - IF "kind" NOT in context
* => FALSE
* - IF context.kind !== "class" | "field" | "getter" | "setter" | "accessor" | "method"
* => FALSE
* - IF "access" NOT in context
* && context.kind NOT !== "class"
* => FALSE
* - IF keyof context.access !== "has" | "get" | "set"
* => FALSE
* - IF "addInitializer" NOT in context
* || typeof context.addInitializer !== "function"
* => FALSE
* - IF context.kind === "class"
* && IF typeof context.name !== "string" | "undefined"
* => FALSE
* ELSE IF typeof context.name !== "string" | "symbol"
* => FALSE
* - IF context.kind !== "class"
* && IF ( "static" NOT in context || "private" NOT in context )
* => FALSE
* - IF context.kind === ( "method" || "getter" || "setter" )
* && IF typeof target !== "function"
* => FALSE
* - IF context.kind === "accessor"
* && IF typeof target !== "object"
* || "get" NOT in target
* || "set" NOT in target
* => FALSE
* - IF context.kind === "field"
* && IF typeof target !== "undefined"
* => FALSE
* ...
* - IF you are still here...
* => TRUE :)
*/
static isDecoratorArgs<T extends Decorator = Decorator<any, any>>(
it: unknown,
): it is DecoratorArgs<T> {
if (it == null || typeof it !== "object" || !Array.isArray(it)) {
return false;
}
if (it.length !== 2) return false;
const [target, context] = it as DecoratorArgs<T>;
if (!("kind" in context)) return false;
const { kind } = context;
if (
!["class", "field", "getter", "setter", "accessor", "method"].includes(
kind,
)
) return false;
if (
!("access" in context) || !("name" in context) ||
!("addInitializer" in context &&
typeof context.addInitializer === "function")
) return false;
if (
!("access" in context) || context.access == null ||
typeof context.access !== "object"
) {
return false;
}
if (kind === "field" && target !== undefined) return false;
if (
!Reflect.ownKeys(context.access).every((k) =>
["has", "get", "set"].includes(String(k))
)
) {
return false;
}
return true;
}
static use<const T extends Record<string, (...args: any) => any>>(
decorators: T,
): Chained<T> {
return new Chainable({ ...decorators }) as Chained<T>;
}
private stack: (readonly [name: string, decorator: Decorator])[] = [];
private decorators: Record<string, DecoratorOrDecoratorFactory> = {};
constructor(decorators: T) {
super("return this");
const chain = new Proxy(this, {
apply: (_target, _thisArg, args) => {
const composed = this.decorate.call(this);
if (Chainable.isDecoratorArgs(args)) {
return Reflect.apply(composed, this, args);
} else {
return (target: any, context: any) => composed(target, context);
}
},
get: (target, p) => {
if (has(target.decorators, p)) {
return Reflect.get(this.decorators, p);
}
switch (p) {
case "constructor":
return Chainable;
case "stack":
return this.stack;
case "decorators":
return this.decorators;
case "decorate":
return this.decorate.bind(this);
default: {
if (has(this, p)) {
const v = this[p];
if (typeof v === "function") return v.bind(this);
return v;
}
const v = Reflect.get(target, p);
if (typeof v === "function") return v.bind(target);
return v;
}
}
},
ownKeys: (target) => {
return [
...new Set(
Reflect.ownKeys(target.decorators).concat(Reflect.ownKeys(target)),
),
];
},
getPrototypeOf: () => {
return Chainable.prototype;
},
has: (target, p) => {
return has(target.decorators, p) || has(target, p);
},
});
for (let name in decorators) {
const _name = name;
const fn = decorators[name];
let i = 1;
while (has(chain, name) || has(Chainable.prototype, name)) {
name = `${_name}${++i}` as typeof name;
}
this.decorators[name] = new Proxy<DecoratorOrDecoratorFactory>(
(...args: any[]) => fn(...args),
{
apply: (_that, _thisArg, args) => {
if (Chainable.isDecoratorArgs(args)) {
const D: Decorator<T, any> = fn;
chain.stack.push([name, D]);
return chain;
} else {
const F: Decorator<T, any> = Reflect.apply(fn, undefined, args);
chain.stack.push([name, F]);
return chain;
}
},
get: (target, p, receiver) => {
switch (p) {
case "stack":
return this.stack;
case "decorate":
return this.decorate.bind(this);
case name: {
if (!has(chain, name) && has(target, name)) {
return Reflect.get(target, name);
}
return Reflect.get(target, name, receiver);
}
default: {
if (has(chain.decorators, p)) {
return Reflect.get(chain.decorators, p);
}
if (has(target, p)) {
return Reflect.get(target, p) ??
Reflect.get(target, p, receiver);
}
if (has(chain, p)) {
return Reflect.get(chain, p);
}
return Reflect.get(target, p, receiver);
}
}
},
ownKeys: (target) => {
return [
...new Set(
Reflect.ownKeys(chain.decorators).concat(
Reflect.ownKeys(target),
).filter((k) =>
k !== name && !chain.stack.find(([name]) => k === name)
),
),
];
},
has: (target, p) => {
return has(target, p) || has(chain.decorators, p);
},
getPrototypeOf: () => Chainable.prototype,
},
);
}
return chain;
}
public decorate<T, V>(): IsomorphicDecorator<T, V> {
const stack = [...new Set(this.stack)]; // dedupe
this.stack.length = 0;
return decorator;
function decorator(target: any, context: any): any {
let result: any = target; // the value to be decorated
const accessorResult: ClassAccessorDecoratorResult<any, any> = {
get: undefined!,
set: undefined!,
init: undefined!,
};
if (context) context.metadata ??= {};
let done = false;
while (!done) {
// apply decorators in reverse order, as per spec
const entry = stack.pop()!;
if (!entry) return result;
const [_decoratorName, fn] = Array.from(entry);
if (typeof fn === "function") {
try {
// all the added factories and decorators are pushed to the stack as
// plain ol decorators, so they can be called via the same signature.
// any args passed into a decorator factory are applied to it, and
// the returned decorator is then added to the stack :)
let newTarget = target;
if (
context.kind === "class" ||
context.kind === "method" ||
context.kind === "getter" ||
context.kind === "setter"
) {
if (typeof result === "function") newTarget = result;
result = fn(newTarget as never, context) ?? result;
} else if (context.kind === "accessor") {
if (typeof result === "object") {
const {
get = accessorResult.get ??= function (this: T) {
return context.access.get(this);
},
set = accessorResult.set ??= function (this: T, v: V) {
context.access.set(this, v);
},
} = result ?? {};
newTarget = { get, set };
}
const newResult = fn(newTarget as never, context) ?? result;
if (typeof newResult === "object" && newResult != null) {
if ("get" in newResult && typeof newResult.get === "function") {
accessorResult.get = newResult.get;
}
if ("set" in newResult && typeof newResult.set === "function") {
accessorResult.set = newResult.set;
}
if (
"init" in newResult && typeof newResult.init === "function"
) {
accessorResult.init = newResult.init;
}
}
result = accessorResult;
} else if (context.kind === "field") {
result = fn(undefined!, context) ?? result;
} else {
throw new TypeError("Invalid decorator context.");
}
} catch (e) {
// gracefully handle errors in applying the decorators
console.warn(e);
continue;
}
}
if (stack.length === 0 && fn == undefined) {
done = true;
return result;
}
}
return result;
}
}
[decoratorName: string | symbol]: unknown;
}
import { Chainable } from "./chainable.ts";
import type {
ClassAccessorDecoratorFunction,
ClassMethodDecoratorFunction,
ClassGetterDecoratorFunction,
ClassSetterDecoratorFunction,
ClassFieldDecoratorFunction,
ClassDecoratorFunction,
Is,
} from "./helpers.ts";
// improved debugging experience
declare global {
interface CallSite {
getThis(): unknown;
getFunctionName(): string | undefined;
getTypeName(): string | null;
getMethodName(): string | null;
getLineNumber(): number;
getColumnNumber(): number;
getFileName(): string | null;
isEval(): boolean;
getEvalOrigin(): string | undefined;
isToplevel(): boolean;
isPromiseAny(): boolean;
isPromiseAll(): boolean;
isConstructor(): boolean;
isAsync(): boolean;
getPromiseIndex(): number | null;
}
interface ErrorConstructor {
stackTraceLimit: number;
prepareStackTrace: (error: Error, stack: CallSite[]) => unknown;
}
}
Error.stackTraceLimit = 100;
Error.prepareStackTrace = (error, stack) =>
stack.map((site) => {
const callsite = {
error,
this: site.getThis(),
functionName: site.getFunctionName(),
typeName: site.getTypeName(),
methodName: site.getMethodName(),
lineNumber: site.getLineNumber(),
columnNumber: site.getColumnNumber(),
evalOrigin: site.getEvalOrigin(),
fileName: site.getFileName(),
isEval: site.isEval(),
isToplevel: site.isToplevel(),
isConstructor: site.isConstructor(),
};
console.log(callsite);
return callsite;
});
// for downleveling to targets lower than esnext
if (!("metadata" in Symbol) || typeof Symbol.metadata !== "symbol") {
Object.defineProperty(Symbol, "metadata", {
value: Symbol("Symbol.metadata"),
});
}
const SymbolMetadata: unique symbol = (Symbol as any).metadata;
const MEMOIZE_CACHE = Symbol("MEMOIZE_CACHE");
const chain = Chainable.use({
log<T extends string>(label: T) {
return (target: unknown, context: DecoratorContext) => {
console.log(label, target, context);
return target;
};
},
memo,
bound<T extends object, V extends (this: T, ...args: any) => any>(): (
target: V,
context: ClassMethodDecoratorContext<T, V>,
) => V {
return (target, context) => {
const { kind, name, addInitializer, private: isPrivate } = context || {};
if (typeof context !== "object" || context == null) {
throw new TypeError(
"Expected Stage 3 decorator support but encountered a legacy implementation at runtime. " +
"Check your TypeScript version - 5.0+ is required. If using Deno, v1.40.0+ is required.",
);
}
if (kind !== "method") {
throw new TypeError(
"@bound is not supported on non-method/non-function class members.",
);
}
if (isPrivate) {
throw new TypeError("@bound is not supported on private methods.");
}
addInitializer(function (this: any) {
this[name] = this[name]?.bind?.(this);
const type = typeof this[name];
if (type !== "function") {
throw new TypeError(
`Expected "${
String(name)
}" to be a function, but received: ${type}.`,
);
}
});
return target;
};
},
enumerable<T extends boolean>(value: T) {
return function decorator<This, V>(
target: V,
context: DecoratorContext,
): void {
if (typeof context !== "object" || context == null) {
throw new TypeError(
"Expected Stage 3 decorator support but encountered a legacy implementation at runtime. Check your TypeScript version (5.0+ is required).",
);
}
const { kind, name, addInitializer } = context;
const v = !!value;
addInitializer(function (this: any) {
if (name && name in this) {
const descriptor = Reflect.getOwnPropertyDescriptor(this, name) ?? {};
if (kind === "getter") {
descriptor.get ??= target as () => V;
} else if (kind === "setter") {
descriptor.set ??= target as (v: V) => void;
} else if (kind === "accessor") {
descriptor.get ??=
(target as ClassAccessorDecoratorTarget<This, V>).get;
descriptor.set ??=
(target as ClassAccessorDecoratorTarget<This, V>).set;
} else {
descriptor.get = descriptor.set = undefined!;
descriptor.value ??= target;
descriptor.writable ??= true;
}
descriptor.enumerable = v;
descriptor.configurable = true;
if (["getter", "setter", "accessor"].includes(kind)) {
delete descriptor.writable;
delete descriptor.value;
} else {
delete descriptor.get;
delete descriptor.set;
}
Reflect.defineProperty(this, name, descriptor);
}
});
return target as any;
};
},
});
class Foo {
@chain.log("Foo: hiding instance field...").enumerable(false)
foobar: string = "1";
@chain.log("Foo: binding proto method...").bound()
myFunction() {
return this.foobar;
}
accessor rgba = [234, 234, 234, 1] as readonly [
number,
number,
number,
number?,
];
accessor xyz = [0.456, 0.123, 0.987] as readonly [number, number, number];
@chain.log("hsla").enumerable(true).memo("rgba")
get hsla() {
const [r, g, b] = this.rgba;
return [(r / 255) * 360, (g / 255) * 100, (b / 255) * 100];
}
@chain.log("Foo: memoizing lab").memo("xyz").enumerable(true)
get lab() {
const [x, y, z] = this.xyz;
return [x * 360, y * 127, z * 100];
}
}
const foo = new Foo();
console.log("rgba", foo.rgba);
console.log("hsla #1", foo.hsla);
console.log("hsla #2", foo.hsla, "(memoized based on rgba's value)");
console.log("changing the rgba value...", foo.rgba = [100, 100, 100]);
console.log("hsla #3", foo.hsla);
console.log("hsla #3", foo.hsla);
console.log("xyz", foo.xyz);
console.log("lab #1", foo.lab);
console.log("lab #2", foo.lab, "(memoized based on xyz's value)");
console.log("changing the xyz value...", foo.xyz = [0.1, 0.1, 0.1]);
console.log("lab #3", foo.lab);
console.log("lab #4", foo.lab);
console.log(foo);
console.log(JSON.stringify(Object.getOwnPropertyDescriptors(foo), null, 2));
export type Memoized<T extends object, K extends keyof T | T[keyof T]> = T & {
[MEMOIZE_CACHE]?: WeakMap<T, Map<K, unknown>>;
};
export function clearCache<
const T extends {
readonly [SymbolMetadata]: { [MEMOIZE_CACHE]: WeakMap<object, unknown> };
},
>(obj: T): boolean;
export function clearCache(obj: object): boolean;
export function clearCache(obj: object): boolean {
if (typeof obj === "object" && obj != null && !Array.isArray(obj)) {
try {
if (SymbolMetadata in obj) {
const metadata =
(obj[SymbolMetadata] ??= Object.create(null)) as Record<
PropertyKey,
unknown
>;
if (
MEMOIZE_CACHE in metadata &&
metadata[MEMOIZE_CACHE] instanceof WeakMap
) {
if (metadata[MEMOIZE_CACHE].has(obj)) {
metadata[MEMOIZE_CACHE].delete(obj);
return true;
} else {
return false;
}
} else {
(metadata[MEMOIZE_CACHE] = new WeakMap())!.set(obj, new Map());
return true;
}
}
} catch {
return false;
}
}
return false;
}
/**
* Decorator for memoizing a method or getter based on a cache key. If called
* as a decorator factory, the first argument is used to create the cache key:
* if the key is a string and it is the name of an existing property on the
* target object, the value of that property is used as the cache key (so the
* decorated method or getter will return a cached value so long as the value
* of the specified property does not change. if the value changes, the cache
* is cleared and the method or getter will be called and cached again). If
* the key is a function, it is called with the target object as its only
* argument, and the return value of that function is used as the cache key.
*/
export function memo<
This extends object,
Key extends PropertyKey & {} | keyof This,
Value,
>(
key: Key | (() => Key),
): {
(
getter: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
): ((this: This) => Value) | void;
};
export function memo<
This extends object,
Key extends PropertyKey & {} | keyof This,
Value extends (this: This, ...args: any) => any,
>(
key: Key | (() => Key),
): {
// deno-lint-ignore no-explicit-any
(
method: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
};
export function memo<This, Value>(
getter: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
): ((this: This) => Value) | void;
// deno-lint-ignore no-explicit-aeny
export function memo<This, Value extends (this: This, ...args: any) => any>(
method: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
export function memo<This, Value>(
...args:
| readonly [label: PropertyKey | (() => PropertyKey)]
| readonly [
target: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
]
| readonly [
method: Value,
context: ClassMethodDecoratorContext<
This,
Is<Value, (this: This, ...args: any) => any>
>,
]
): typeof args extends
readonly [(this: This) => Value, ClassGetterDecoratorContext<any, any>]
? ((this: This) => Value) | void
: typeof args extends readonly [Value, ClassMethodDecoratorContext<any, any>]
? Is<Value, (this: This, ...args: any) => any> | void
:
| ClassGetterDecoratorFunction<This, Value>
| ClassMethodDecoratorFunction<This, Value>
| void {
if (args.length === 1) {
const [target] = args;
if (typeof target === "function") {
return memoizeDecoratorFactory(target());
} else if (
typeof target === "string" ||
typeof target === "symbol" ||
typeof target === "number"
) {
return memoizeDecoratorFactory(target);
}
} else if (args.length === 2) {
const [target, context] = args as readonly any[];
memoizeDecoratorFactory()(target, context);
}
return memoizeDecoratorFactory();
}
function memoizeDecoratorFactory<
const K extends PropertyKey | (() => PropertyKey),
Key extends PropertyKey = K extends PropertyKey ? K
// deno-lint-ignore no-explicit-any
: K extends ((...args: any) => infer R extends PropertyKey) ? R
: never,
>(cacheKey?: K): {
<This, Value>(
target: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
): ((this: This) => Value) | void;
<This, Value>(
target: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>,
): ClassAccessorDecoratorFunction<This, Value> | void;
<This, Value extends (this: This, ...args: unknown[]) => unknown>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): Value | void;
} {
return function memoizeDecorator<
This extends object,
// deno-lint-ignore no-explicit-any
Value,
>(
target:
| ClassAccessorDecoratorTarget<This, Value>
| Is<Value, (this: This, ...args: unknown[]) => unknown>
| ((this: This) => Value),
context:
| ClassAccessorDecoratorContext<This, Value>
| ClassMethodDecoratorContext<
This,
Is<Value, (this: This, ...args: unknown[]) => unknown>
>
| ClassGetterDecoratorContext<This, Value>,
): any {
const { name, kind, metadata = Object.create(null) } = context;
metadata[MEMOIZE_CACHE] ??= new WeakMap();
const caches = metadata[MEMOIZE_CACHE] as WeakMap<This, Map<Key, Value>>;
const getCacheKey = (thisArg: This, args?: ArrayLike<unknown>): Key => {
let key: Key = "" as Key;
if (
typeof cacheKey === "string" ||
typeof cacheKey === "number" ||
typeof cacheKey === "symbol"
) {
// deno-lint-ignore no-explicit-any
key = cacheKey in thisArg ? (thisArg as any)[cacheKey] : cacheKey;
}
if (typeof cacheKey === "function") {
key = cacheKey.call(thisArg) as Key;
}
return [name.toString(), key.toString(), ...Array.from(args ?? [])].join(
":",
) as Key;
};
const getCachedValue = (
thisArg: This,
args?: ArrayLike<unknown>,
): Value => {
const cache: Map<Key, Value> = caches.get(thisArg) ??
caches.set(thisArg, new Map()).get(thisArg)!;
const key = getCacheKey(thisArg, args);
let value = cache.get(key)!;
if (value === undefined) {
if (kind === "method" || kind === "getter") {
value = (target as Function).apply(thisArg, args);
} else if (kind === "accessor") {
value = (target as ClassAccessorDecoratorTarget<This, Value>).get
.call(thisArg)!;
} else {
throw new TypeError("Invalid decorator context");
}
cache.set(key, value);
}
return value;
};
if (kind === "method") {
return function (this: This, ...args: unknown[]) {
return getCachedValue(this, args);
};
} else if (kind === "getter") {
return function (this: This) {
return getCachedValue(this);
};
} else if (kind === "accessor") {
return {
get() {
return getCachedValue(this);
},
set(value: Value) {
clearCache(metadata);
context.access.set(this, value);
},
} satisfies ClassAccessorDecoratorResult<This, Value>;
} else {
throw new TypeError("Invalid decorator context");
}
};
}
// #region Types
export interface ClassDecoratorFunction<T = any, V = any> {
<This extends T, Value extends Is<V, AbstractConstructor<This>>>(
target: Value,
context: ClassDecoratorContext<Value>,
): void | Value;
}
export interface ClassFieldDecoratorFunction<T = any, V = any> {
<This extends T, Value extends V>(
target: undefined,
context: ClassFieldDecoratorContext<This, Value>,
): void | ((this: This, initialValue: Value) => Value);
}
export interface ClassMethodDecoratorFunction<T = any, V = any> {
<This extends T, Value extends Is<V, (this: This, ...args: any) => unknown>>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): void | Value;
}
export interface ClassAccessorDecoratorFunction<T = any, V = any> {
<This extends T, Value extends V>(
target: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>,
): void | ClassAccessorDecoratorResult<This, Value>;
}
export interface ClassGetterDecoratorFunction<T = any, V = any> {
<This extends T, Value extends V>(
target: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
): void | ((this: This) => Value);
}
export interface ClassSetterDecoratorFunction<T = any, V = any> {
<This extends T, Value extends V>(
target: (this: This, newValue: Value) => void,
context: ClassSetterDecoratorContext<This, Value>,
): void | ((this: This, newValue: Value) => void);
}
export type Decorator<T = any, V = any> =
| ClassDecoratorFunction<T, V>
| ClassFieldDecoratorFunction<T, V>
| ClassMethodDecoratorFunction<T, V>
| ClassAccessorDecoratorFunction<T, V>
| ClassGetterDecoratorFunction<T, V>
| ClassSetterDecoratorFunction<T, V>;
export type DecoratorFactory<
This = any,
Value = any,
Args extends readonly any[] = any,
> = {
<const A extends Args>(...args: A): Decorator<This, Value>;
};
export type DecoratorOrDecoratorFactory<
This = any,
Value = any,
Args extends readonly any[] = any,
> = Decorator<This, Value> & DecoratorFactory<This, Value, Args>;
export type DecoratorArgs<D extends Decorator = Decorator<any, any>> = D extends
(target: infer T, context: infer C extends DecoratorContext) => any
? readonly [target: T, context: C]
: Parameters<D>;
export interface IsomorphicDecorator<T, V> {
<Class extends AbstractConstructor<V>>(
target: Class,
context: ClassDecoratorContext<Class>,
): void | Class;
<This extends T, Value extends V>(
target: undefined,
context: ClassFieldDecoratorContext<This, Value>,
): void | ((this: This, initialValue: Value) => Value);
<This extends T, Value extends Is<V, (this: This, ...args: any) => any>>(
target: Value,
context: ClassMethodDecoratorContext<This, Value>,
): void | Value;
<This extends T, Value extends V>(
target: (this: This) => Value,
context: ClassGetterDecoratorContext<This, Value>,
): void | ((this: This) => Value);
<This extends T, Value extends V>(
target: (this: This, newValue: Value) => void,
context: ClassSetterDecoratorContext<This, Value>,
): void | ((this: This, newValue: Value) => void);
<This extends T, Value extends V>(
target: ClassAccessorDecoratorTarget<This, Value>,
context: ClassAccessorDecoratorContext<This, Value>,
): void | ClassAccessorDecoratorResult<This, Value>;
}
// #endregion Types
export type Is<T, U = unknown> = T extends U ? U extends T ? U : T
: [(T & U)] extends [never] ? (U & Omit<T, keyof U>)
: (T & U);
export type Constructor<T = any, A extends any[] = any[]> = new (
...args: A
) => T;
export type Class<
Prototype = any,
Args extends any[] = any[],
Static extends {} = {},
> =
& Constructor<Prototype, Args>
& { prototype: Prototype }
& {
[K in keyof Static as [Static[K]] extends [never] ? never : K]:
& ThisType<Class<Prototype, Args, Static>>
& Static[K];
};
export type AbstractConstructor<T = any, A extends any[] = any[]> =
abstract new (...args: A) => T;
export type AbstractClass<
Prototype = any,
Args extends any[] = any[],
Static extends {} = {},
> =
& AbstractConstructor<Prototype, Args>
& { prototype: Prototype }
& {
[K in keyof Static as [Static[K]] extends [never] ? never : K]:
& ThisType<AbstractClass<Prototype, Args, Static>>
& Static[K];
};
/** UnionToIntersection<{ a: string } | { b: string }> = { a: string } & { b: string } */
export type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => 0 : never
) extends (arg: infer I) => 0 ? I
: never;
/** LastInUnion<1 | 2> = 2 */
export type LastInUnion<U> = UnionToIntersection<
U extends unknown ? (x: U) => 0 : never
> extends (x: infer L) => 0 ? L
: never;
/** UnionToTuple<1 | 2> = [1, 2] */
export type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never] ? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
export function has<const T extends object, K extends PropertyKey>(
o: T,
k: K,
own?: boolean,
): o is T & { [P in K]: K extends keyof T ? T[K] : unknown };
export function has(o: object, k: PropertyKey, own?: boolean): boolean;
export function has(o: object, k: PropertyKey, own?: boolean): boolean {
if (o != null && typeof o === "object") {
if (
typeof k === "string" || typeof k === "symbol" || typeof k === "number"
) {
return own ? Object.hasOwn(o, k) : Reflect.has(o, k);
}
}
return false;
}
const $bind = Function.prototype.bind;
export function bindSafe<
T,
const A extends readonly unknown[],
const B extends readonly unknown[],
R = unknown,
// deno-lint-ignore no-explicit-any
U extends Record<string | symbol, any> = Record<never, never>,
>(
target: ((...args: [...A, ...B]) => R) & U,
thisArg: T,
...args: A
): ((this: T, ...args: B) => R) & typeof target {
const props = Object.getOwnPropertyDescriptors(target);
const value = target.name;
const fn = $bind.call(target, thisArg, ...args);
const length = Math.max(target.length - args.length, 0);
Reflect.defineProperty(fn, "name", { value, configurable: true });
Reflect.defineProperty(fn, "length", { value: length, configurable: true });
Reflect.defineProperty(fn, "toString", {
value: function toString(this: typeof target) {
let str = Function.prototype.toString.call(this);
const check = () => str.includes("[native code]");
if (check()) str = this.toString();
if (check()) str = `function anonymous() { [native code] }`;
return str;
}.bind(target),
});
for (const key in props) {
// skip the constructor and name properties
if (key === "constructor") continue;
const descriptor = props[key];
if (!descriptor || !descriptor.configurable) continue;
const { value, get, set } = descriptor;
if (value && typeof value === "function") {
// bind static methods to the original thisArg
// - note: this uses the new bind method, not the original, meaning it
// may recursively bind any nested methods all the way down the chain
descriptor.value = bindSafe(value, target);
} else {
// bind static getters/setters to the original thisArg
// - note these use the original Function.prototype.bind since they don't
// require any special logic here.
if (get && typeof get === "function") {
descriptor.get = $bind.call(get, target);
}
if (set && typeof set === "function") {
descriptor.set = $bind.call(set, target);
}
}
Reflect.defineProperty(fn, key, descriptor);
}
return fn;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment