Issue
I'm working on a factory and I need to eventually add custom methods. I'm able to not add the custom methods using overloads, but when I start adding the custom methods nothings works as desired. :(
I'm able to successfully cover the case where no custom methods are provided and the case where the custom methods argument has a wrong type.
type Base = { id: string }
type Methods<T> = { [key: string]: (this: T) => unknown };
function factory<T extends Base>(method: (this: T) => void): (new () => T);
function factory<
T extends Base & M,
M extends Methods<T>
>(method: (this: T) => void, methods: M): (new () => T);
function factory<
T extends Base & M,
M extends Methods<T>
>(method: (this: T) => void, methods?: M): (new () => T) {
return null as unknown as (new () => T);
}
// Ok: T = Base
factory(function() {
this.id = this.a();
});
// Ok: 0 is not valid value for argument methods (we don't care about T in this case)
factory(function() {
this.id = this.a();
}, 0);
While if I pass a valid value to the custom methods argument, nothing is working! The playground is a powerful tool check problems details.
// Nothing working as desired
factory(function() {
this.id = this.a();
}, {
a: function(): string {
this.a();
this.b();
return "0";
}
});
If we mouseover on the factory
function name we can see its type is:
function factory<Base & Methods<unknown>, {
a: (this: Base & Methods<unknown>) => string;
}>
so we can say that
type T = Base & Methods<unknown>
type M = { a: (this: Base & Methods<unknown>) => string; };
here is the problem: since T
is something, why M
is resolved as Methods<unknown>
rather than Methods<something>
?
There are many other problems (the b
method is not considered error, the a
method is considered to return unknown
rather than string
as the a
property of the Object passed as methods
argument, etc.) but it seems they are all side effects of the wrong M
type resolution.
I strongly suspect that the root cause of all these problems is the circular dependencies between T
and M
and between T
and itself, but I can't do without because
T
needs to depend onM
to give it the custom methods;M
needs to depend onT
to infer the type of the implicit this argument to the custom methods.
Any idea about how implement this?
Solution
TypeScript resolves/infers generics from left to right.
In this case, you need to infer Methods
first and only then infer method
itself.
I just replaced first and second argument:
type Base = { id: string }
const factory = <
T extends Base,
Methods extends Record<string, <Self extends Methods>(this: T & Self) => unknown>,
>(methods: Methods, method: (this: T & Methods) => void) => {
return null as any
}
factory({
a: function () {
this.id = this.a(); // Ok: string
const b = this.b(); // Ok: number
const c = this.c(); // Ok: method does not exists
return "0";
},
b: function () {
this.id = this.a(); // Ok: string
const b = this.b(); // Ok: number
const c = this.c(); // Ok: method does not exists
return 0;
},
}, function () {
this.id = this.a(); // Ok: string
const b = this.b(); // Ok: number
const c = this.c(); // Ok: method does not exists
});
In order to infer this
of each method I have used extra generic parameter <Self extends Methods>(this: T & Self) => unknown
If you wonder why I have added Self
and did not use just Methods
please see this answer.
P.S. I have a rule of thumb: If you have some problem with arguments inference - add one more generic. Usually it helps :D
Answered By - captain-yossarian
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.