microsoft/TypeScript

union types are not strict enough

MastroLindus opened this issue · 6 comments

TypeScript Version: 2.4.0/2.2.1

Code

type A = {type: "a", aValue: "foo"};
type B = {type: "b", bValue: "bar"};

type C = A | B;

const foobar = (param: C) => console.log(param);

foobar({type: "a", bValue: 3, aValue: "foo"});

const testValue : C = {type: "a", bValue: 3, aValue: "foo"};

Expected behavior:
Invocation of foobar is wrong, since {type: "a", bValue: 3, aValue: "foo"} is neither a valid A nor a valid B

Assignment of testValue declared as C to {type: "a", bValue: 3, aValue: "foo"} should also be wrong, but it is not.

Actual behavior:
Union types apparently allow any field of the union members to be present, and their type is not even checked but interpreted as any (bValue should only allow "bar" value, but you can pass 3 or any other value and it won't complain).
Similarly, it would be really great if, after typing 'type: "a",', the autocomplete would start suggesting only the fields compatible (in this case, aValue) instead of all the ones in the union.

I thought that this might have something to do with function parameters being bivariant in typescript, but I would at least expect the assignment "const testValue : C = {type: "a", bValue: 3, aValue: "foo"};" to fail because it really looks wrong...

Invocation of foobar is wrong, since {type: "a", bValue: 3, aValue: "foo"} is neither a valid A nor a valid B

That value is definitely a valid A - it has a matching type property and a matching aValue property. It happens to be a subtype of A due to the extra property, but it's a structural type system and you are allowed to have other properties.

You can make an explicitly disjoint union by declaring that A and B can't have those properties:

type A = {type: "a", aValue: "foo", bValue?: never };
type B = {type: "b", bValue: "bar", aValue?: never };

Assuming that this is correct, I find really confusing that this:

const testValue : C = {type: "a", anotherUnmentionedField: 3, aValue: "foo"};

won't work, since according to the rule, it is supposed to also be a valid A.

I would assume that either the compiler should be strict about only allowing known properties of the type it is inferring, or instead it should always allow other additional properties to be present.

This rule about allowing other properties to be present, but only if they happen to come from other members of the union, even disregarding their type (treating them as any), doesn't make much sense to me in this case.

However for my specific use-case, your workaround would actually work, but the autocomplete would still not help (it would still suggest you every single field, including the never ones). Maybe it could be proposed to never add to the autocomplete fields of type never | undefined ?

I created #16759 to deal with the autocomplete issue since I think it's a separate and independent one.

I still think thought that this issue should not be "working as intended" for the reason described above:
either we should be allowed to specify any additional field as long as the required interface is satisfied, or we shouldn't be allowed to specify anything more than the interface.

The fact that for unions we are allowed to specify fields of the other types composing the union feels wrong to me. Any comments on that @RyanCavanaugh ?

Ref: #12936

I was not aware of the discussion around exact types.
In that discussion it is mentioned that

For the most part where people want exact types, we'd prefer to fix that by making EPC smarter. A key area here is when the target type is a union type - we want to just take this as a bug fix (EPC should work here but it's just not implemented yet).

That seems to be describing the issue here: EPC working with normal types but not with union types.

If exact types would be added to the language I guess my use cases for this issue should be covered.

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