Aaron Moore Aaron Moore - 1 month ago 23
TypeScript Question

Typescript Union Type stops distinguishing once a custom class is included in the union

I have found that the typescript 2.0.3 compiler will compile without complaint an invalid assignment if the union type in question includes a user defined class of any kind.

Example:

class Bar {}

// Complains as expected:
interface Foo {
bar: number|string
}

// Does not complain, surprisingly:
interface Foo2 {
bar: number|string|Bar
}

var a = <Foo> {
bar: 5
};
var a2 = <Foo> {
bar: "yar"
};
//var a3 = <Foo> {
// bar: new Date() // compiler complains as expected.
//};

var b = <Foo2> {
bar: new Date() // compiler does not complain, I am confused.
};


The compiler error I get when I uncomment
a3
is:

lib/src/thing.ts(18,10): error TS2352: Type '{ bar: Date; }' cannot be converted to type 'Foo'.
Types of property 'bar' are incompatible.
Type 'Date' is not comparable to type 'string | number'.
Type 'Date' is not comparable to type 'number'.


I would expect to receive the same error when assigning
b
, but it compiles just fine without complaint.

Is this a known issue? Or is this expected behavior and I am not seeing why this should be considered valid? I would like to be able to rely on the union type to ensure that the property is one of several things, including classes I defined myself, so any insights would be most appreciated.

Thanks, in advance!




Edit: I did a little more testing and came up with an even simpler example:

class Bar {}

var a = <string|number> 4;
var b = <string|number> "thing";
var c = <string|number> new Bar(); // works: confusing
var d = <Bar> 4; // ... confusing
var f = <number> new Bar(); // ... also confusing

Answer

Typescript is using duck typing, as written in the docs:

One of TypeScript’s core principles is that type-checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”

Since your Bar class is empty, the compiler manages to match the Date object with Bar, because there are no contradictions.
But once you add a member or method to Bar you'll get an error:

class Bar {
    x: number;
}

var b = <Foo2> {
    bar: new Date()
};

Produces:

Type '{ bar: Date; }' cannot be converted to type 'Foo2'.
  Types of property 'bar' are incompatible.
    Type 'Date' is not comparable to type 'string | number | Bar'.
      Type 'Date' is not comparable to type 'Bar'.
        Property 'x' is missing in type 'Date'.

(code in playground)