Typescript: Union types could be reduced to their 'minimum valid' definitions

Created on 20 Jun 2017  ·  14Comments  ·  Source: microsoft/TypeScript

TypeScript Version: 2.4.0

Code

// The following works no problem
type ExhaustiveType = Array<number | undefined>;

const test1 = (value: ExhaustiveType) => value.filter(v => v === 2);

/** Cannot invoke an expression whose type lacks a call signature. Type '{ (callbackfn: (this: void, value: number | undefined, index: number, array: (number | undefined)...' has no compatible call signatures. */
type ExcessiveType = number[] | Array<number | undefined>;

const test2 = (value: ExcessiveType) => value.filter(v => v === 2);

Expected behavior:
Both examples compile.

Actual behavior:
The latter example fails with Cannot invoke an expression whose type lacks a call signature. Type '{ (callbackfn: (this: void, value: number | undefined, index: number, array: (number | undefined)...' has no compatible call signatures.

Explanation
ExhaustiveType and ExcessiveType both represent exactly the same types. The latter is more explicit, and while in this particular example it might look silly, w/ more complex types and type aliases, being more explicit allows for more expressivity. IMHO TypeScript could at the time of defining ExcessiveType, 'reduce it down' to the ExhaustiveType. Alternatively (to preserve expressiveness e.g. in the IDE), the compiler could internally treat them as equivalent.

Needs More Info

All 14 comments

I'm sorry, I don't work here or anything, just wanted to test your code, cause it seems to me like Array<number | undefined> !== number[].

So I test it here

However, I can't reproduce that issue you mention. It compiles both types as number[] (witch for me it's an unexpected behavior cause you explicit said that could be undefined).

Moreover, if you set strictNullCheck the error you said are getting threw, so that's a necessary thing to know.

Also, if you use intersection type instead, it works as you expected. Maybe that's what you wanted to do?

The reduction of union types to a constituent that is a common supertype already happens. Perhaps something is missing in your example?

@RyanCavanaugh - Typescript is treating both of the types in my example differently (one's blowing up, another one doesn't). So it seems like I've managed to find some edge case maybe?

Since Array<number | undefined> is not the same as number[], then isn't this issue an instance of #1805?
(If OP switches to number[] to (number | undefined)[] then the union is reduced and everything works.)

@jcalz - sure, but number[] | Array<number | undefined> is the same type as Array<number | undefined>

No, not with strictNullChecks it isn't:

var x: number[] | Array<number | undefined>;
var x: Array<number | undefined>; // error: 
// Subsequent variable declarations must have the same type.  
// Variable 'x' must be of type '(number | undefined)[] | number[]', 
// but here has type '(number | undefined)[]'.

EDIT: wait, maybe I see where you're going with this.

Each value of type number[] | (number | undefined)[] should be a value of type (number | undefined)[] and vice versa, right? So the type checker isn't making that inference here.

Okay, so you're saying that number[] is a subtype of (number | undefined)[] and that the union should absorb the subtype. I don't know that subtype collapsing is actually happening (I put this in #16386 but it's kind of languishing there). Anyway, yeah, I think I get your point now @kujon.

@RyanCavanaugh

The reduction of union types to a constituent that is a common supertype already happens.

Are you sure of this?

@jcalz - precisely this. (number | undefined)[] is a super type of number[] so by definition, super_type union sub_type == super_type

This error only happens if you set strictNullCheck, so I don't think this is an error at all, cause it is checking that (number | undefined) !== number because they are not the same if you are checking for null and undefined types.

EDIT: Also, in this environment, (number | undefined) are not a super type of number cause number are strictly treated as such, and can't be undefined nor null.

However, this works as expected if the compiler is not in strict mode.

Example with primitives only

Toggle strictNullCheck to see the error and the union typing changes

@michaeljota no, this has nothing to do with undefined. Let's try a different one:

var k = [1, 2, 3];
(k as number[]).filter(x => typeof x === 'string'); // fine
(k as (number | string)[]).filter(x => typeof x === 'string'); // fine
(k as number[] | (number | string)[]).filter(x => typeof x === 'string'); // error!

The typechecker is not absorbing number[] into (number | string)[], even though all values of type number[] are also values of type (number | string)[].

EDIT: however, arrays are mutable... so therefore number[] might not be a subtype of (number | string)[] if you intend to mutate anything. I think ts does some unsound bivariant things with parameters because of this. Blecch.

however, arrays are mutable... so therefore number[] might not be a subtype of (number | string)[] if you intend to mutate anything.

If I understand the last message correctly, that would also mean that number[] is not number[] anymore, right?

Maybe? Since typescript is perfectly fine assigning a value of type number[] to a variable of type (number|string)[] but not vice versa, then I guess there is a subtype relationship there, even though you can do horrible things like

var a: number[] = [1,2,3];
var b: (number | string)[] = a;
b[0] = 'boom';
console.log(a[0]);

So forget my previous edit, I guess.

This is somewhat irrelevant to the issue at hand, but technically number[] is not a subtype of (number | undefined)[], because the latter supports inserting undefined members and the former doesn't. Array<T> is not covariant wrt T, although something like ReadOnlyArray<T> should be.

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kyasbal-1994 picture kyasbal-1994  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

manekinekko picture manekinekko  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments

fwanicka picture fwanicka  ·  3Comments