ts-delegate
Version:
Helper annotations, types, and methods for type-safe and JSDoc-preserving delegation in TypeScript
146 lines (145 loc) • 4.72 kB
JavaScript
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]);
};
}