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:
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
inoperator has always produced unsound narrowings due to structural subtyping: see this comment
https://github.com/Microsoft/TypeScript/pull/15256#discussion_r154843152
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
inoperator 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.