Reputation: 75
I'm currently in the middle of an extensive TypeScript exercise (typescript-exercises.github.io, exercise 14), and I seem to be getting stuck on what looks to me like a typescript bug. But since I'm not that experience with TypeScript I'll first ask here to see if it really is a bug, or just me being stupid.
In this exercise you need to add types to a bunch of functions that except any amount of arguments, and return subfunctions if the amount of arguments is insufficient. Problem here is that TypeScript doesn't seem to get the generics right.
If I check the type of func
used in the last line in my IDE, it's read as func<*, number>
, with * being the complete type of add instead of just number.
Am I doing something wrong or is this really TypeScript acting up?
declare function func<T1, T2>(subFunc: (arg1: T1, arg2: T2) => T1);
function add(a: number, b: number): number;
function add(a: number): (b: number) => number;
function add(): typeof add;
function add(a?: number, b?: number) {
if (a === undefined)
return add;
if (b === undefined) {
return function subAdd(b?: number) {
if (b === undefined)
return subAdd;
return a + b;
}
}
return a + b;
}
console.log( func(add) );
The actual exercise prohibits changing the test (last line), so manually supplying the generics is unfortunately not an option.
P.S. This is a simplification of the actual exercise. If you want to look at the origin, this is the add function being used as the reducer in one of the tests in test.ts.
Upvotes: 0
Views: 197
Reputation: 330216
It's not exactly a bug, but it is due to a design limitation of TypeScript.
The issue you're hitting is the same as in microsoft/TypeScript#27027; when the compiler tries to infer from an overloaded function type (a function with multiple call signatures), it only examines the last call signature. This is instead of what you want, which is for the compiler to choose an overload signature based on how it will/would be called; which the compiler unfortunately cannot do; hence the design limitation.
So in func(add)
, what does not happen is: the compiler sees that add
needs to match type (arg1: T1, arg2: T2) => T1
, so it selects the call signature (a: number, b: number) => number
because that's the first one that matches. Therefore T1
gets inferred as number
, and T2
also gets inferred as number
.
What does happen is: the compiler sees that add
needs to match type (arg1: T1, arg2: T2) => T1
. Because add
is an overloaded function, it ignores everything except the last call signature, which is () => typeof add
. Now this does match (arg: T1, arg2: T2) => T1
, because functions of fewer parameters are assignable to functions of more parameters (TL;DR it is generally safe for functions to ignore extra parameters they are called with, and there are lots of times when this is desirable). There is therefore no useful inference site for either argument, but there is a good inference site for the return type, T1
. And so the compiler infers that T1
is typeof add
, and T2
is unknown
because there's nothing better for it to infer (type parameters are implicitly constrained to unknown, and when generic type inference fails the compiler gives up and widens to the constraint).
And so you see func<typeof add, unknown>
(unknown
, not number
, right?) and you're sad.
I'm not sure what, if anything, should be done to your code to deal with this. It's not clear to me why func()
needs to operate on add
at all, and generally speaking trying to use overloaded functions in higher order positions tends to run into the limitation you see here. Overloaded functions are really meant to be called directly and that's it.
For this particular issue you could deal with it by reversing the order of your overloads, like
declare function add(): typeof add;
declare function add(a: number): (b: number) => number;
declare function add(a: number, b: number): number;
which will cause T1
and T2
to be inferred as number
as desired:
console.log(func(add));
// function func<number, number>(subFunc: (arg1: number, arg2: number) => number): void
but as soon as you try some slightly different func
-like thing that needs to access a different call signature, you'll have the same problem again.
Upvotes: 2