Skip to content

Union Types lead to TypeError #29508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tmkn opened this issue Jan 21, 2019 · 7 comments
Closed

Union Types lead to TypeError #29508

tmkn opened this issue Jan 21, 2019 · 7 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@tmkn
Copy link

tmkn commented Jan 21, 2019

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

@fatcerberus
Copy link

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.

@lsagetlethias
Copy link

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.

@fatcerberus
Copy link

fatcerberus commented Jan 21, 2019

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.

@jack-williams
Copy link
Collaborator

jack-williams commented Jan 21, 2019

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.

@fatcerberus
Copy link

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/

@tmkn
Copy link
Author

tmkn commented Jan 21, 2019

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. ¯_(ツ)_/¯

@fatcerberus
Copy link

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
#15256 (comment)

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Jan 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

6 participants