I have a variable with a type that is a union of different array types. I want to narrow its type to a single member of that union, so I can run the appropriate code over it for each type. Using Array.every
and a custom type guard seems like the right approach here, but when I try this TypeScript complains that "This expression is not callable." along with an explanation that I don't understand.
Here is my minimum reproducible example:
const isNumber = (val: unknown): val is number => typeof val === 'number';
const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];
if (unionArr.every(isNumber)) { // <- Error
unionArr;
}
Here is the error:
This expression is not callable.
Each member of the union type
'{
<S extends string>(predicate: (value: string, index: number, array: string[]) => value is S, thisArg?: any): this is S[];
(predicate: (value: string, index: number, array: string[]) => unknown, thisArg?: any): boolean;
} | {
...;
}'
has signatures, but none of those signatures are compatible with each other.
This isn't preventing me from continuing. I've found that using a type assertion to recast my array to unknown[]
before I narrow its type, as I would do if I were writing an isNumberArray
custom type guard, removes the error without compromising type safety.
I've also found that recasting my string[] | number[]
array to (string | number)[]
removes the error.
However, the type of the array doesn't seem to be narrowed correctly, so I would need to use an additional as number[]
after the check:
const isNumber = (val: unknown): val is number => typeof val === 'number';
const unionArr: string[] | number[] = Math.random() > 0.5 ? [1, 2, 3, 4, 5] : ['1', '2', '3', '4', '5'];
if ((unionArr as unknown[]).every(isNumber)) { // <- No error
unionArr; // <- Incorrectly typed as string[] | number[]
}
if ((unionArr as (string | number)[]).every(isNumber)) { // <- No error
unionArr; // <- Incrrectly typed as string[] | number[]
}
I tried a comparison with a non-array union as well, though of course in this case I was just using the custom type guard directly instead of using it with Array.every
. In that case, there was also no error and the type was narrowed correctly:
const isNumber = (val: unknown): val is number => typeof val === 'number';
const union: string | number = Math.random() > 0.5 ? 1 : '1';
if (isNumber(union)) {
union; // <- Correctly typed as number
}
Because I have that safe type assertion workaround, I can continue without needing to understand this. But I'm still very confused as to why that error appears in the first place, given I am trying to narrow a union of types to a single member of that union.
I'm guessing this is something to do with how Array.every
has been typed by TypeScript, and there's probably nothing I can do aside from the workaround I'm already using. But it's hard to be sure of that when I don't really understand what's going wrong. Is there something I could do differently here, or is the as unknown[]
type assertion I've used the correct or best way to handle this?
Let's take a simpler example:
type StringOrNumberFn = (a: string | number) => void
type NumberOrBoolFn = (a: number | boolean) => void
declare const justNumber: StringOrNumberFn | NumberOrBoolFn
// works
justNumber(123)
// errors
justNumber('asd')
justNumber(true)
When you have a union of functions that you try to invoke, you are actually calling a function that is the intersection of those members. If you don't know which function it is, then you may only call that function in ways that both functions support. In this case, both functions can take a number
, so that's all that's allowed.
And if the intersection of those functions would have incompatible arguments, then the function cannot be called. So let's model that:
type StringFn = (a: string) => void
type NumberFn = (a: number) => void
declare const fnUnion: StringFn | NumberFn
fnUnion(123) // Argument of type 'number' is not assignable to parameter of type 'never'.(2345)
fnUnion('asdf') // Argument of type 'string' is not assignable to parameter of type 'never'.(2345)
This is closer to your problem.
A string array's every
and a number array's every
are typed to receive different parameters.
(value: string, index: number, array: string[]) // string[] every() args
(value: number, index: number, array: number[]) // number[] every() args
Which is essentially the same problem as above.
So I don't think there's a way that the compiler will be okay with calling the every
method on this union.
Instead, I'd probably write a type assertion for the whole array and loop over it manually.
const isNumberArray = (array: unknown[]): array is number[] => {
for (const value of array) {
if (typeof value !== 'number') return false
}
return true
}
declare const unionArr: string[] | number[]
if (isNumberArray(unionArr)) {
Math.round(unionArr[0]); // works
}