Skip to content

Breaking Change: Unexpected narrowing to never when "in" guard used on intersection #38162

Closed
@RyanCavanaugh

Description

@RyanCavanaugh

TypeScript Version: 3.8.3 vs nightly

Search Terms: intersection in operator

Code

type MaybeFoo = {
    bar: string;
    foo?: number;
};
type AlwaysFoo = {
    foo: number | undefined;
    always: string;
};

declare const f: AlwaysFoo & MaybeFoo;
if ("foo" in f) {
    f.bar;
} else {
    // 3.8.3: f: AlwaysFoo & MaybeFoo
    // 3.9: 'f' is 'never'
    f.bar;
}

Expected behavior: Well...

This is probably technically the correct behavior. However, for the specific case of window, this is problematic. There are globals like ontouchstart which are declared as being var ontouchstart: whatever | undefined in the global scope; these get intersected with the global Window type, and because we believe "variables can't be optional", we start narrowing to never in the else branch here.

It's unclear what we should do differently here, but this caused a break in a few different places in real world code where people are doing feature detection. We're incorrectly telling people that e.g. ontouchstart is always present, even though you would get an error to unconditionally invoke it (because it is indeed possibly-undefined).

Since there's still no "optional var" and still no "missing vs undefined", we should maybe treat properties that include undefined as "possibly not in" for the purposes of narrowing.

Actual behavior: Described

Playground Link: Link

Related Issues: Possible root cause: #37106

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions