Is it possible to use generic type parameter in its own extends constrains?

83 views Asked by At

I want to use generic type parameter in its own constraint. It is possible at all in typescript?

My code is here:

type Config<T> = {
    context: T;
};

type Hooks<T> = {
    hooks: T;
};

type FunctionWithThis<T> = (this: T, ...args: any[]) => any;

type RemoveThis<T extends Record<string, FunctionWithThis<any>>> = {
    [P in keyof T]: T[P] extends (...a: infer A) => infer R ?  (...a:A) => R: never
}

 const configure = <TContext extends Object, 
                    THooks extends Record<string, FunctionWithThis<TContext & THooks>>> // problem here
                   (config: Config<TContext> & Hooks<THooks>) => {
    const result = {
        get data() { return config.context; }
    };

    Object.entries(config.hooks).forEach((action) => {
        (result as any)[action[0]] = (...args: any[]) => action[1].call(config.context as any, ...args);
    });

    return result as { data: TContext; } & RemoveThis<THooks>;
};

const engine = configure({
    context: {
        foo: 12
    },
    hooks: {
        log() {
            console.log(this.foo); // this.foo is typed correctly here but I don't have access to another hooks
        },
        test(str: string) {

        }
    }
});

Or in the typescript playground

I'm trying to create a configuration function for executing set of functions with a predefined context. I've managed to create a simple demo version but now I would like to have an ability to call my hooks from another hook. E.g. I want to configure my test hook to call log hook. In order to achieve this I'm trying to pass a union type as a generic parameter to the 'FunctionWithThis' type:

FunctionWithThis<TContext & THooks>

But unfortunately it doesn't give me what I want: I still have intellisense for context by my hooks are unavailable. It seems like generic parameter is resolved to unknown when it's used as a constraint for itself.

Is it a way to overcome this?

Actually I have even more complex plans: I want to add one more generic parameter to configure function and for callbacks and also would like to have an ability to call callbacks from hooks and vise versa. So it would look like this: THooks extends Record<string, FunctionWithThis<TContext & THooks & TCallbacks>>> where TCallbacks is a new generic parameter which follows THooks

1

There are 1 answers

4
jcalz On BEST ANSWER

Your specific problem is hard to solve with just generics. Luckily, TypeScript has a special magical type function called ThisType as implemented in microsoft/TypeScript#14141 which allows the this context of object literal methods to be specified.

I'd expect your code to be typed like this:

type ConfigwithHooks<C, H> = {
    context: C;
    hooks: H & ThisType<C & H>; // Type of 'this' in hooks is C & H
};

const configure = <C, H>(config: ConfigwithHooks<C, H>) => {
    const result = {
        get data() { return config.context; }
    };

    Object.entries(config.hooks).forEach((action) => {
        (result as any)[action[0]] = (...args: any[]) => action[1].call(config.context as any, ...args);
    });

    return result as { data: C } & H;
};

This works the way you want, I think:

const engine = configure({
    context: {
        foo: 12
    },
    hooks: {
        log() {
            console.log(this.foo);
            this.test("hello");
        },
        test(str: string) {
            this.foo - 5;
        }
    }
});
    
/* const engine: {
    data: {
        foo: number;
    };
} & {
    log(): void;
    test(str: string): void;
} */

const data = engine.data;
engine.log();

I haven't looked into how to get your more complex plans implemented, but I wanted to make sure you had a way forward on the code in the question as asked.

Playground link to code