TypeScript Version: 2.0.2rc
Code
const foo = (): number[] | string[] => Math.random() > 0.5 ? [0, 1, 2] : ["0", "1", "2"];
const bar = foo().filter((x) => Number(x) % 2 === 1);
Expected behavior:
No errors.
Actual behavior:
Compiler gives: error TS2349: Cannot invoke an expression whose type lacks a call signature.
This issue seems to be array-specific. The following code compiles as expected:
const foo = (): { a } | { b } => Math.random() > 0.5 ? { a: true } : { b: true }
const bar = foo().hasOwnProperty('a');
I think this would be because there is no way to infer a common type between Array<number> and Array<string>. The definition of Array#filter is scoped to the T in Array<T>. I'm guessing a compiler exception would have to be made to infer T = Foo | Bar when calling (Foo[] | Bar[]).filter (and other methods).
Workaround:
const foo = (): (number | string)[] => ...
(Note this unveils an expected second error in the snippet because number % number yields number and .filter expects boolean)
@jwbay That works, but it's semantically different - [0, "1", 2] matches that type declaration, but not the one in my original example.
(Also, you're right - it compiles but the reliance on implicit type coercion was lazy. I've amended my example.)
This is a bit subtle, but number[] | string[] !=== (number | string)[]. The first one has an assert on homogeneity, while the second does not. just as in the example noted above by @kaotik4266, [0, "1", 2] is not of type number[] | string[].
There is not a meaningful way to describe the general case for merging unmatching signatures; while filter might be arguably safe, push is not. So merging the signatures of (number[]|string[]).push(...) th same proposed for filter would result in push(a: number | string); which means that we are treating the original type as (number | string)[], which is not type safe.
So the general case, is there is no common signatures between the two types, and thus the resulting union type does have a filter property but it does not have a signature, and hence the error message Cannot invoke an expression whose type lacks a call signature.
I think the more natural intuition is not to try to merge the signatures into one at all. Instead, they should be tried one after the other for conformance with the given arguments and if a call is not possible, then that's a type error according to the existing rules. This way an appropriate massage can be displayed as an error (i.e. the offending signature) and the return type is a union of all return types.
With the above example in mind:
const foo: number[] | string[] = null as any;
The signature for filter which the compiler suggests (and is 100% correct) is:
filter: ((calbackFn: (value: string, index: number, array: string[]) => any) => string[])
| ((calbackFn: (value: number, index: number, array: number[]) => any) => number[])
And the provided function is compatible with both overloads, thus this call should be valid.
foo.filter((x: string|number): boolean => Number(x) % 2 === 1)
// => string[] | number[] (union of the return types of both overloads)
On the other hand, the signature for push is:
push: ((...items: string[]) => number)
| ((...items: number[]) => number)
Clearly foo.push('hello'); is a type violation, because the second overload is not met. The expected error should be something like:
Type '((...items: string[]) => number) | ((...items: number[]) => number)' is not invokable with argument of type 'string'.
Type '((...items: number[]) => number)' is not invokable with argument of type 'string'.
Argument of type 'string' is not assignable to parameter of type 'number'
I suspect a signature unifying solution might be also possible, but it would be much harder to implement, will have to take into account variances and deriving a digestible error messages would be much harder.
@mhegazy I don't know if there is already an issue to track this, but in general, given a union of two function signatures:
let f: ((foo: Foo) => void) | ((foo: Bar) => void);
It should be possible to invoke to invoke the function with arguments that are assignable to an intersection of each parameter type:
let x: Foo & Bar;
f(x); // This is sound, and should be allowed
In the case given above, Foo and Bar are Predicate<string> and Predicate<number>, respectively, and Predicate<any> satisfies Predicate<string> & Predicate<number>, so this should be allowed.
I feel like this is a special case of a more general structural subtyping rule, but can't quite formulate how.
@masaeedu there are simple answers for one-off cases but your proposed fix doesn't address the case where there are multiple signatures in each union constituent
@RyanCavanaugh By multiple signatures, do you mean multiple arguments, or are you referring to overloads?
Overloads
Okay, lets see. Here's an overloaded function signature:
type ThisOrThat<This, That> = {
(arg: This): void;
(arg: That): void;
}
let f: ThisOrThat<Foo, Bar> | ThisOrThat<Baz, Quux>;
In this case, continuing the suggested pattern of allowing intersections and recalling that the implementation of an overloaded function must be written to deal with a union of each argument's types in every overload:
let x: (Foo | Bar) & (Baz | Quux);
f(x); // This should be allowed
This at least handwavily makes sense; if the argument implements the required interface for at least one overload from both possible function signatures, then it is acceptable to pass it to f regardless of which of the function signatures actually inhabits f.
@RyanCavanaugh Not to derail the discussion, but this does bring up the tangentially related issue of why does this not compile?
function flipflop(arg: number): string
function flipflop(arg: string): number
function flipflop(arg: number | string): number | string {
if (typeof arg === 'string') {
return arg.length;
} else {
return arg.toString();
}
}
let x: number | string;
let result = flipflop(x);
I would expect it to compile and for result to have type number | string.
Overloads are intersections, not unions. It's closer to impl1 & impl2 semantically than impl1 | impl2, although I'm proposing merging overloads with intersections.
Overloads are intersections, not unions.
@isiahmeadows That may be so, but when you're invoking an overloaded function, each positional argument must be assignable to a union of the corresponding positional parameter types of the constituent functions, not an intersection. This is why I said, I would expect
let x: number | string;
let result = flipflop(x);
to compile. That said, I do think removing "overloads" as a distinct concept in favor of type algebra on functions is a good idea. You may be interested in this: https://github.com/Microsoft/TypeScript/issues/1805#issuecomment-255482792
@isiahmeadows Sorry, I think I misunderstood which comment you were responding to. I think you might have been responding to:
I don't know if there is already an issue to track this, but in general, given a union of two function signatures ...
In that case, yes; overloads are not unions. However it should still be possible to construct a union of two functions and invoke it (with an intersection of their argument types). Put another way, overloads are merely a special case of the ability to do type algebra with function types.
See also #7294
Most helpful comment
This is a bit subtle, but
number[] | string[]!===(number | string)[]. The first one has an assert on homogeneity, while the second does not. just as in the example noted above by @kaotik4266,[0, "1", 2]is not of typenumber[] | string[].There is not a meaningful way to describe the general case for merging unmatching signatures; while
filtermight be arguably safe,pushis not. So merging the signatures of(number[]|string[]).push(...)th same proposed forfilterwould result inpush(a: number | string); which means that we are treating the original type as(number | string)[], which is not type safe.So the general case, is there is no common signatures between the two types, and thus the resulting union type does have a
filterproperty but it does not have a signature, and hence the error messageCannot invoke an expression whose type lacks a call signature.