UNPKG

ts-delegate

Version:

Helper annotations, types, and methods for type-safe and JSDoc-preserving delegation in TypeScript

146 lines (145 loc) 4.72 kB
function setField(self, candidate, key, metadata) { const [get, set] = metadata.get(key); Object.defineProperty(self, key, { get: get ? () => candidate[key] : () => undefined, set: set ? (v) => candidate[key] = v : () => { }, }); } /** * Adds instanced delegated methods to a class; called during runtime * @param self Instance to add delegated methods to * @param delegateCandidates Array of instances for delegation * * @example * ```ts * class ClassOne { * n: number; * constructor() { * this.n = Math.random(); * } * @DelegateMethod() * myMethod(): void { * console.log("Hello world!", this.n); * } * } * class ClassTwo implements Delegate<[ClassOne]> { * constructor() { * delegateStatic(this, [new ClassOne()]); * } * * // This will copy over type safety and JSDoc * declare myMethod: ClassOne['myMethod']; * } * ``` */ export function delegate(self, delegateCandidates) { for (const candidate of delegateCandidates) { const proto = Object.getPrototypeOf(candidate); for (const key of Object.getOwnPropertyNames(proto)) { if (!candidate[key].isDelegated) { continue; } self[key] = candidate[key].bind(candidate); } if (!delegationMetadata.has(proto)) continue; const metadata = delegationMetadata.get(proto); for (const key of metadata.keys()) { // define properties setField(self, candidate, String(key), metadata); } } } /** * Adds static delegate methods to a class; called during runtime * @param self Instance to add delegated methods to * @param delegateCandidates Array of classes for delegation, this is the name of the class and not with .prototype or typeof * * @example * ```ts * class ClassOne { * @DelegateMethod() * static myMethod(): void { * console.log("Hello world!"); * } * } * class ClassTwo implements DelegateStatic<[ClassOne], ClassTwo> { * constructor() { * delegateStatic(this, [ClassOne]); * } * * // This will copy over type safety and JSDoc * declare myMethod: ClassOne['myMethod']; * } * ``` */ export function delegateStatic(self, delegateCandidates) { for (const candidate of delegateCandidates) { const metadata = delegationMetadata.get(candidate); for (const key in candidate) { const val = candidate[key]; if (typeof val != "function") { if (metadata == undefined) continue; if (!metadata.has(key)) continue; setField(self, candidate, key, metadata); continue; } if (!val.isDelegated) continue; self[key] = (...args) => val.apply(candidate, val.omitFirst ? [self, ...args] : args); } } } /** * Annotate methods with this to mark them to delegate into classes * @param omitFirst If true, the first argument of this method will be omitted when delegating * to the target method. It will be replaced with the instance of the class that delegated it. * This only applies for static methods. * * @example * ```ts * class Example { * @DelegateMethod() // an instance delegating this method will be able to use this.exampleMethod(a, b); * exampleMethod(a: number, b: number): number { * return a + b; * } * @DelegateMethod(true) // an instance delegating this method will be able to use this.editSelf(); * static editSelf(self: any): void { * self.something = Math.random(); * } * } * ``` */ export function DelegateMethod(omitFirst = false) { return (_, __, descriptor) => { descriptor.value.isDelegated = true; descriptor.value.omitFirst = omitFirst; }; } const delegationMetadata = new WeakMap(); /** * Annotate fields with this to mark them to delegate into other classes * @param get If the field can be retrieved from the delegated class * @param set If the field can be set from the delegated class * * @example * ```ts * class Example { * @DelegateField() // an instance delegating this method will be able to use this.field1 * field1: number = 100; * * @DelegateField(true, false) // it can be read from the delegated class, but not set * field2: string = "CONSTANT"; * } * ``` */ export function DelegateField(get = true, set = true) { return (target, propertyKey, _) => { if (!delegationMetadata.has(target)) { delegationMetadata.set(target, new Map()); } delegationMetadata.get(target).set(propertyKey, [get, set]); }; }