Issue
So, imagine I have a (dumb) function like this:
function doSomething(input: number|string): boolean {
if (input === 42 || input === '42') {
return true;
} else {
return false;
}
}
Why am I allowed to call array.some()
like this:
function doSomethingWithArray(input: number[]|string[]): boolean {
return input.some(i => doSomething(i));
}
But not array.every()
like this:
function doEverythingWithArray(input: number[]|string[]): boolean {
return input.every(i => doSomething(i));
}
Which gives me this error:
This expression is not callable. Each member of the union type '{ (predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; (predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; } | { ...; }' has signatures, but none of those signatures are compatible with each other.
I don't understand the difference. In my mind either both should work, or neither. What am I missing?
Note that doSomething()
accepts number|string
as its argument, so it should work with every element of number[]|array[]
, like it does for array.some()
, shouldn't it?
Solution
This is essentially a current limitation in TypeScript; see microsoft/TypeScript#44373.
TypeScript has long had a problem with calling methods on a union of array types. Originally you couldn't call unions-of-functions at all, because the compiler didn't know how to merge multiple signatures into a single usable one. See microsoft/TypeScript#7294:
// Before TS3.3:
declare const arr: string[] | number[];
arr.some(() => true); // error
arr.map(() => 1); // error
arr.every(() => true); // error
TypeScript 3.3 introduced some support for doing this with microsoft/TypeScript#29011, by accepting an intersection of the parameters from the union members.
But this support was only added for relatively simple cases where at most one member of the union was a generic function and at most one member of the union was an overloaded function. So if the array methods you were merging were generic or overloaded, you still couldn't call them. So there was an improvement, but some methods like map()
still weren't callable. See microsoft/TypeScript#36390:
// TS3.3
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // okay
At some point it was decided to add a generic overload to every()
so that it could act as a type guard function; see microsoft/TypeScript#38200. This is useful, but unfortunately it means that every()
also stopped working on unions of array types. This was released with TypeScript 4.0:
// TS 4.0
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // error
arr.every(() => true); // error
For TypeScript 4.2 microsoft/TypeScript#31023 was merged, which added some support for calling unions of generic call signatures, as long as the generic type parameters were identical. But for overloaded call signatures, the problem persists:
// TS 4.2+
declare const arr: string[] | number[];
arr.some(() => true); // okay
arr.map(() => 1); // okay
arr.every(() => true); // error
And that's where we are currently.
Maybe at some point, microsoft/TypeScript#44373 will be addressed. It's not obvious what the right thing to do here is for unions of overloaded methods in general. I think it's possible someone might want to target just the unions-of-array case by automatically widening to a readonly-array-of-unions, since doing that manually is the workaround I usually suggest, and it's fairly safe:
const arr2: readonly (string | number)[] = arr; // okay
arr2.every(() => true); // okay
But for now, this doesn't happen.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.