Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions tsc/internal/checker/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ func (c *Checker) narrowTypeByDiscriminantProperty(t *Type, access *ast.Node, op
candidate := c.getConstituentTypeForKeyType(t, c.getTypeOfExpression(value))
if candidate != nil {
if assumeTrue && operator == ast.KindEqualsEqualsEqualsToken || !assumeTrue && operator == ast.KindExclamationEqualsEqualsToken {
return candidate
return c.addNullableConstituents(candidate, t)
}
if propType := c.getTypeOfPropertyOfType(candidate, keyPropertyName); propType != nil && isUnitType(propType) {
return c.removeType(t, candidate)
Expand All @@ -722,6 +722,13 @@ func (c *Checker) narrowTypeByDiscriminantProperty(t *Type, access *ast.Node, op
})
}

// Nullable constituents have no key property, so the general discriminant narrowing logic in
// narrowTypeByDiscriminant always retains them in branches where the discriminant can match. Add
// them back to a constituent selected through the key property map to produce the same result.
func (c *Checker) addNullableConstituents(candidate *Type, t *Type) *Type {
return c.getUnionType([]*Type{candidate, c.filterType(t, func(t *Type) bool { return t.flags&TypeFlagsNullable != 0 })})
Comment thread
z0rimo marked this conversation as resolved.
}

func (c *Checker) narrowTypeByDiscriminant(t *Type, access *ast.Node, narrowType func(t *Type) *Type) *Type {
propName, ok := c.getAccessedPropertyName(access)
if !ok {
Expand Down Expand Up @@ -1241,7 +1248,7 @@ func (c *Checker) narrowTypeBySwitchOnDiscriminantProperty(t *Type, access *ast.
return c.unknownType
}))
if candidate != c.unknownType {
return candidate
return c.addNullableConstituents(candidate, t)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ if (working.thing!.name !== "Correct") {
>log : (...data: any[]) => void
>working.thing!.id : string
>working.thing! : { name: 'Correct'; id: string; }
>working.thing : { name: 'Correct'; id: string; }
>working.thing : { name: 'Correct'; id: string; } | undefined
>working : WorkingType
>thing : { name: 'Correct'; id: string; }
>thing : { name: 'Correct'; id: string; } | undefined
>id : string
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @strict: true
// @noEmit: true
// @noTypesAndSymbols: true

// A non-null assertion narrows only the asserted expression itself; it does not establish a
// persistent non-null flow fact for the underlying reference. This must hold identically for
// unions small enough to use the fallback discriminant narrowing path and unions large enough
// to use the optimized key-property path, so that a `!` assertion behaves consistently
// regardless of union size (#62511). Optional chaining remains the idiomatic way to narrow the
// reference itself and continues to do so unchanged.

type Small =
| { type: "1" }
| { type: "2" }
| undefined;

type Large =
| { type: "1" }
| { type: "2" }
| { type: "3" }
| { type: "4" }
| { type: "5" }
| { type: "6" }
| { type: "7" }
| { type: "8" }
| { type: "9" }
| { type: "10" }
| undefined;

type LargeWithNull = Exclude<Large, undefined> | null;

// Strict equality, both branches.

declare let smallEqual: Small;
if (smallEqual!.type === "1") {
// @ts-expect-error
smallEqual.type;
} else {
// @ts-expect-error
smallEqual.type;
}

declare let largeEqual: Large;
if (largeEqual!.type === "1") {
// @ts-expect-error
largeEqual.type;
} else {
// @ts-expect-error
largeEqual.type;
}

// Strict inequality.

declare let smallNotEqual: Small;
if (smallNotEqual!.type !== "1") {
// @ts-expect-error
smallNotEqual.type;
}

declare let largeNotEqual: Large;
if (largeNotEqual!.type !== "1") {
// @ts-expect-error
largeNotEqual.type;
}

// Switch.

declare let smallSwitch: Small;
switch (smallSwitch!.type) {
case "1":
// @ts-expect-error
smallSwitch.type;
break;
}

declare let largeSwitch: Large;
switch (largeSwitch!.type) {
case "1":
// @ts-expect-error
largeSwitch.type;
break;
}

// `null` constituents are preserved just like `undefined`.

declare let largeNullEqual: LargeWithNull;
if (largeNullEqual!.type === "1") {
// @ts-expect-error
largeNullEqual.type;
}

declare let largeNullSwitch: LargeWithNull;
switch (largeNullSwitch!.type) {
case "1":
// @ts-expect-error
largeNullSwitch.type;
break;
}

// Control: optional chaining is unaffected and remains the idiomatic way to narrow the
// reference itself, for both union sizes.

declare let smallOptional: Small;
if (smallOptional?.type === "1") {
smallOptional.type;
} else {
// @ts-expect-error
smallOptional.type;
}

declare let largeOptional: Large;
if (largeOptional?.type === "1") {
largeOptional.type;
} else {
// @ts-expect-error
largeOptional.type;
}