Typescript: Union Types lead to TypeError

Created on 21 Jan 2019  Β·  7Comments  Β·  Source: microsoft/TypeScript


TypeScript Version: 3.3.0-dev.201xxxxx


Search Terms:

Code

interface A {
    a: string;
}

interface IFoo {
    blabla: number;
}

interface B {
    b: string;
    foo: IFoo;
}

interface C {
    c: string;
    foo: IFoo;
}

type ABC = A | B | C;

let test: ABC = {
    a: "",
    c: ""
}

if("c" in test) {
    console.log(test.foo.blabla);   ///Uncaught TypeError: Cannot read property 'blabla' of undefined
}

Expected behavior:
It shouldn't narrow down the type to C since it doesn't match the shape of C.
Actual behavior:
Compiles fine, even with strict on
Playground Link:
Link
Related Issues:
I think this is related:

20863

Question

Most helpful comment

This _is_ a bug to the extent that the linked issue is a bug (#20863), which is essentially a part duplicate.
The in operator has always produced unsound narrowings due to structural subtyping: see this comment by @RyanCavanaugh for related discussion.

This comment by @weswigham explains one currently accepted way of dealing with the problem -- you want to specify the properties that _should not_ be present as optionally undefined.

All 7 comments

I'm not 100% sure this is a bug - the compiler knows test can be one of A, B, C (the initialization matches A and excess-property checking doesn't work with unions IIRC), then you do a check "c" in test which acts as a type guard. The only member of the union with a c property is C, so the compiler narrows it to C within the if block.

If there's a bug at all, it's that "c" in test is an incomplete type guard--but I have a hunch that might be by design.

I don't think this is a bug.

in operator is a builtin TypeGuard + "interpreter guard", that infer whatever is built on your union type or in your value directly in JS.

Here, you're only admit that because c is present, it's enough to say that typeof test is C from ABC's union.

Maybe this is more what you want to achieve:


function isC(obj: ABC): obj is C {
    return "c" in test && "foo" in test;
}

if (isC(test)) {
    console.log(test.foo.blabla);
} else {
    console.log('Safe passed.');
}

Also, if A interface had a c property, the in operator would infer your type as A | C. That's why custom TypeGuard is more useful in your case.

For the record, even if it's not a bug, I can see how this can cause issues. TypeScript is structurally typed and excess properties create a subtype - which means that, to use this case as an example, the object is actually an A but the type guard maps it to C because that's the only interface with c explicitly declared. However it's perfectly legal for an A to also have an extra c property without making it into a C itself.

This _is_ a bug to the extent that the linked issue is a bug (#20863), which is essentially a part duplicate.
The in operator has always produced unsound narrowings due to structural subtyping: see this comment by @RyanCavanaugh for related discussion.

This comment by @weswigham explains one currently accepted way of dealing with the problem -- you want to specify the properties that _should not_ be present as optionally undefined.

Somewhat off-topic but just going to throw in that preventing all runtime errors due to incorrect typing ("soundness") isn't even a design goal, and in fact doing so is not even practical. This article might be illuminating:
https://www.brandonbloom.name/blog/2014/01/08/unsound-and-incomplete/

That TypeScript narrows it down to C due to the in operator check is definitely a bug as this is simply not true.
Property blabla is completely missing nor was it initialized either, yet TypeScript happily says, yep this is of shapeC (with enabled strictNullChecks & strictPropertyInitialization options!).

At the very least it should infer unknown so you know to check it yourself and not rely on the compiler. Β―_(ツ)_/Β―

It’s a feature, not a bug, as per the comment linked above:

The in operator has always produced unsound narrowings due to structural subtyping: see this comment
https://github.com/Microsoft/TypeScript/pull/15256#discussion_r154843152

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DanielRosenwasser picture DanielRosenwasser  Β·  3Comments

weswigham picture weswigham  Β·  3Comments

jbondc picture jbondc  Β·  3Comments

seanzer picture seanzer  Β·  3Comments

uber5001 picture uber5001  Β·  3Comments