How to replace context type definition for the function

606 views Asked by At

I'm trying to create a configuration function for executing set of functions with a predefined context. Here are my types:

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

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

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

In my configure function I want to merge context and my target functions, but pass context parameter as this to functions:

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

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

    return result as { data: TContext; } & THooks;
};

It can be used like this:

const engine = configure({
    context: {
        foo: 12
    },
    hooks: {
        log() {
            console.log(this.foo); // this.foo is typed correctly here
        }
    }
});

const data = engine.data;
engine.log(); // error here

VS Code gives me following error in the last line:

The 'this' context of type '{ data: { foo: number; }; } & { log(this: { foo: number; }): void; }' is not assignable to method's 'this' of type '{ foo: number; }'. Property 'foo' is missing in type '{ data: { foo: number; }; } & { log(this: { foo: number; }): void; }' but required in type '{ foo: number; }'.

I believe that it means that I'm trying to call log function in the wrong context. Is it possible to change context type for each function in the output?

Or maybe there is another way to solve it?

Typescript Playground

1

There are 1 answers

0
Titian Cernicova-Dragomir On BEST ANSWER

Since THooks must have members of type (this: T, ...args: any[]) => any, the functions on THooks will require this to be of type TContext. And while your function does ensure the this will indeed be of TContext, the types of the retuned functions don't reflect this. So when calling one of these functions on engine, ts looks at the type of enigine and sees it is not the appropriate this for log and issues an error.

Since you pass in this inside your implementation, you need to erase the this parameter from the result. You can do this with a mapped conditional type:

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>>>
                   (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, ...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
        }
    }
});

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

Playground Link