Reputation: 11
this code is from you don't know JS yet, and I'm trying to add types to it, but I'm confused.
function range(start: number, end?: number) {
start = Number(start) || 0;
if (end === undefined) {
return function getEnd(end: number) {
return getRange(start, end);
};
} else {
end = Number(end) || 0;
return getRange(start, end);
}
// **********************
function getRange(start: number, end: number) {
var ret: number[] = [];
for (let i = start; i <= end; i++) {
ret.push(i);
}
return ret;
}
}
range(3, 3); // [3]
range(3, 8); // [3,4,5,6,7,8]
range(3, 0); // []
var start3 = range(3);
var start4 = range(4);
start3(3); // [3]
start3(8); // [3,4,5,6,7,8]
start3(0); // []
start4(6); // [4,5,6]
The error info is:
This expression is not callable. Not all constituents of type '((end: number) => number[]) | number[]' are callable. Type 'number[]' has no call signatures.ts(2349)
I'm trying to find the answer, but still don't understand. I hope you can help me.
Upvotes: 1
Views: 159
Reputation: 330436
The compiler has looked at your return
statements and inferred that the return type of range
is a union type ((end: number) => number[]) | number[]
. So when you call range()
, the compiler knows only that it returns either another function or an array of numbers. It doesn't understand that there is any relationship between how you call range()
and which of those two types the return value will actually be:
const r33 = range(3, 3); // [3]
// const r33: ((end: number) => number[]) | number[]
var start3 = range(3);
// var start3: ((end: number) => number[]) | number[]
See? both r33
and start3
are seen as the same type, something which might be an array, but might be a function instead. It can't see the difference between start3(3)
and r33(3)
.
So if you call start3(3)
, the compiler will warn you that it can't be sure that you're not calling an array as if it were a function. Unless you explicitly check start3
before calling it:
var start3 = range(3);
if (typeof start3 !== "function") throw new Error();
start3(3); // okay
start3(8); // okay
start3(0); // okay
Or unless you just assert that it is a function:
(start4 as Extract<typeof start4, Function>)(6); // okay
Neither of which are what you want to do.
So the compiler can't really infer the relationship between the number of inputs and the output types. But there are ways to tell the compiler this information, so that callers can have an easier time.
Perhaps the most straightforward way is to make range()
an overloaded function. You start by providing a set of call signature declarations, corresponding to the different distinct input-output relationships callers can expect:
// call signatures
function range(start: number, end: number): number[];
function range(start: number): (end: number) => number[];
Then you write the implementation as before:
function range(start: number, end?: number) {
// same impl here
}
This compiles fine, although the compiler does not check it very thoroughly. If, for example, I mixed up the two return types in those call signatures (put number[]
on the second one and (end: number) => number[]
on the first one), the compiler would not catch such a mistake. Overload function implementations are checked more loosely than normal function implementations, so you need to be careful.
Anyway, now callers can call range
and have more reasonable behavior:
const r33 = range(3, 3); // [3]
// const r33: number[]
var start3 = range(3);
// var start3: (end: number) => number[]
Now the compiler knows that r33
is an array and start3
is a function. The calls after this will work as expected.
The other way to do this is to make range()
a generic function whose return type is a conditional type that depends on how the function is called:
function range<T extends [end: number] | []>(start: number, ...[end]: T):
T extends [] ? (end: number) => number[] : number[] {
start = Number(start) || 0;
if (end === undefined) {
return function getEnd(end: number) {
return getRange(start, end);
} as any;
} else {
end = Number(end) || 0;
return getRange(start, end) as any;
}
// same getRange impl here
}
Here we are saying that, after the start
argument, you can expect that the rest of the arguments T
will be assignable either to a single-element tuple corresponding to the end
parameter, or an empty tuple corresponding to no end
parameter. Then the return value of T extends [] ? (end: number) => number[] : number[]
evaluates either to a function type or to an array type depending on T
.
The compiler still cannot follow the logic inside the function to verify that the actual returns comply with this generic-conditional-type (see microsoft/TypeScript#33912 for more information) but unlike overloads the compiler will complain about this. So for now that means you need something like type assertions to make the return statements compile without warning (see the as any
I added), and thus this solution turns out to be quite as unsafe as overloads.
Either way, you need to be careful that the input/output relationship you have expressed by the call signature(s) is actually properly implemented.
Upvotes: 2