ts-delegate
Version:
Helper annotations, types, and methods for type-safe and JSDoc-preserving delegation in TypeScript
166 lines (165 loc) • 5.62 kB
TypeScript
/**
* Adds Intellisense and JSDoc when implemented. This delegates instanced methods from a class.
*
* For the generic, an array of classes should be supplied and the types will be added from them.
*
* @example
* ```ts
* class ClassOne {
* value: number;
*
* constructor() {
* this.value = 0;
* }
*
* @DelegateMethod()
* doSomething() {
* this.value++;
* }
* }
* class ClassTwo implements Delegate<[ClassOne]> {
* value: number = 0;
* constructor() {
* delegate(this, [new ClassOne()]);
*
* this.doSomething();
* }
*
* // Confirms to typescript that it exists
* declare doSomething: ClassOne['doSomething'];
* }
* ```
*/
export type Delegate<T extends any[]> = T extends [infer First, ...infer Rest] ? Partial<{
[K in keyof First]: First[K];
}> & Delegate<Rest> : {};
type IsExactly<T, U> = (<G>() => G extends T ? 1 : 2) extends (<G>() => G extends U ? 1 : 2) ? true : false;
/**
* Omits the first argument from a function
*/
export type OmitFirst<F> = F extends (arg: any, ...rest: infer R) => infer Ret ? (...args: R) => Ret : F;
type OmitFirstIfSelf<F, Self> = F extends (arg: infer First, ...rest: any[]) => any ? IsExactly<First, Self> extends true ? OmitFirst<F> : F : F;
type DelegateFromCtor<C, Self> = Partial<{
[K in keyof C as C[K] extends (...args: any[]) => any ? K : never]: OmitFirstIfSelf<C[K], Self>;
}>;
/**
* Adds Intellisense and JSDoc when implemented. This delegates static methods from a class.
*
* For the first generic, an array of classes should be supplied and the types will be added from them.
*
* And for the second generic, it is the current class itself. This will then omit the first argument of any functions that take self first.
*
* @example
* ```ts
* class ClassOne {
* @DelegateMethod(true)
* static doSomething(two: ClassTwo, amount: number) {
* two.value += amount;
* }
* }
* class ClassTwo implements DelegateStatic<[ClassOne], ClassTwo> {
* value: number = 0;
* constructor() {
* delegateStatic(this, [ClassOne]);
*
* this.doSomething(10); // first argument is omitted and will call with 'this'
* }
*
* // This confirms to typescript that it exists
* // In this case since omitFirst is true, the type here needs to omit the first one to be accurate for the instanced delegated function
* declare doSomething: OmitFirst<ClassOne['doSomething']>;
* }
* ```
*/
export type DelegateStatic<T extends readonly any[], Self> = T extends [infer First, ...infer Rest] ? First extends abstract new (...args: any) => any ? DelegateFromCtor<First, Self> & DelegateStatic<Rest, Self> : DelegateStatic<Rest, Self> : {};
/**
* 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 declare function delegate(self: any, delegateCandidates: any[]): void;
/**
* 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 declare function delegateStatic(self: any, delegateCandidates: any[]): void;
/**
* 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 declare function DelegateMethod(omitFirst?: boolean): MethodDecorator;
/**
* 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 declare function DelegateField(get?: boolean, set?: boolean): PropertyDecorator & MethodDecorator;
export {};