Reputation: 61
The original purpose of this simplified code was to create a generic method in typescript that receive a delegate and an object and returns a new delegate whose signature should be the same of the passed in delegate BUT the "this" context would become the passed in object.
As pseudocode it should be something like this: f(del(args: ...parameters), context) => del(this: typeof context, args: ...parameters).
I tried a few approaches and all failed.
The following code was the attempt that seemed the most promising, but by the end it failed too.
In the code I did highlight my issue. Basically the typescript compiler is not able to infer the type of the arg1 and arg2 automatically looking at the signature of methodWithDelegate function.
Is there an explanation for it?
Is there a solution?
function createDelegate<TContext extends Object, TReturn, A>(fn: (a: A) => TReturn, context: TContext): (this: TContext, a: A) => TReturn
function createDelegate<TContext extends Object, TReturn, A, B>(fn: (a: A, b: B) => TReturn, context: TContext): (this: TContext, a: A, b: B) => TReturn
function createDelegate<TContext extends Object, TReturn, A, B, C>(fn: (a: A, b: B, c: C) => TReturn, context: TContext): (this: TContext, a: A, b: B, c: C) => TReturn
function createDelegate<TContext extends Object, TReturn, A, B, C>(instance: any, funct: Function): any
{
return <any>funct.bind(instance);
}
function methodWithDelegate(delegate: (this: string, val1: string, val2: string) => void)
{
//delegate invokation
}
methodWithDelegate(function (val1, val2)
{
// OK. val1 and val2 are inferred to be strings.
val1.substring(2);
});
methodWithDelegate(createDelegate(function (val1, val2)
{
// ISSUE: val1 and val2 are not inferred to be strings. they remain of type any
val1.substring(2);
}, "CONTEXT"));
Upvotes: 3
Views: 97
Reputation: 328196
I don't have a great answer about why this is happening. You might want to file an issue on Microsoft/TypeScript asking for an explanation. No doubt there's some interplay between generic type parameter inference, function parameter inference, and overload resolution going on.
Obviously one workaround is just to specify the type of val1
and val2
to be string
... that is, give up on the complicated inference game and just ask for explicitly what you need:
methodWithDelegate(createDelegate(function(val1: string, val2: string): void {
val1.substring(2);
}, "CONTEXT"));
In my opinion it's good practice to do explicit annotation like this anyway, since I prefer to state my intentions rather than rely on (possibly incorrect) inference from the compiler. But I understand that you'd prefer something else.
Another workaround is to have no overload, but allow for functions with a large number of optional parameters, like this:
function createDelegate<Ctx extends Object, R, A=never, B=never, C=never, D=never>(fn: (a?: A, b?: B, c?: C, d?: D) => R, context: Ctx): (this: Ctx, a?: A, b?: B, c?: C, d?: D) => R {
return fn.bind(context);
}
Now we are freeing ourselves from dealing with overload resolution, and the inference goes through the way you want. Unfortunately, the function parameters will be optional, so even though it infers the type parameters as string
, the function parameters will be string | undefined
. So you need to handle undefined
in the function, which is why this is a workaround and not a solution:
methodWithDelegate(createDelegate(function(val1, val2) {
// handle the possible undefined issue
if (typeof val1 === 'undefined') throw new Error("No");
// now the following line works for you
val1.substring(2);
}, "CONTEXT"));
Anyway that's the best I can do. Hope it helps; good luck!
Upvotes: 1