Skip to content

Commit 96ca78b

Browse files
committed
Narrow destructured discriminant whose binding has a default
A default on a required discriminant binding no longer disables discriminant narrowing of the sibling bindings. The default is ignored for narrowing only when it can never apply (the property is present and non-undefined in every union constituent), so optional or possibly-undefined discriminants keep their previous sound behavior. Fixes #50139
1 parent 7964e22 commit 96ca78b

5 files changed

Lines changed: 858 additions & 1 deletion

File tree

src/compiler/checker.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29475,7 +29475,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2947529475
if (isIdentifier(expr)) {
2947629476
const symbol = getResolvedSymbol(expr);
2947729477
const declaration = getExportSymbolOfValueSymbolIfExported(symbol).valueDeclaration;
29478-
if (declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.initializer && !declaration.dotDotDotToken) {
29478+
if (
29479+
declaration && (isBindingElement(declaration) || isParameter(declaration)) && reference === declaration.parent && !declaration.dotDotDotToken &&
29480+
(!declaration.initializer || isBindingElement(declaration) && bindingElementDefaultNeverApplies(declaration))
29481+
) {
2947929482
return declaration;
2948029483
}
2948129484
}
@@ -29509,6 +29512,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2950929512
function getCandidateVariableDeclarationInitializer(node: Node) {
2951029513
return isVariableDeclaration(node) && !node.type && node.initializer ? skipParentheses(node.initializer) : undefined;
2951129514
}
29515+
29516+
function bindingElementDefaultNeverApplies(declaration: BindingElement) {
29517+
// A binding element with a default initializer (e.g. `{ kind = "a" }`) only faithfully mirrors its
29518+
// source property for discriminant narrowing when the default can never substitute for a real value,
29519+
// i.e. the property is present and non-undefined in every constituent of the declared union. For an
29520+
// optional or possibly-undefined property the default stands in for `undefined`, so narrowing the
29521+
// parent by this binding would be unsound.
29522+
const name = getAccessedPropertyName(declaration);
29523+
if (name === undefined) {
29524+
return false;
29525+
}
29526+
const prop = getPropertyOfType(declaredType, name);
29527+
return !!prop && !(prop.flags & SymbolFlags.Optional) && !(getCheckFlags(prop) & CheckFlags.Partial) &&
29528+
!maybeTypeOfKind(getTypeOfSymbol(prop), TypeFlags.Undefined);
29529+
}
2951229530
}
2951329531

2951429532
function getDiscriminantPropertyAccess(expr: Expression, computedType: Type) {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
dependentDestructuredVariablesWithDefaults.ts(74,15): error TS2322: Type 'string | number' is not assignable to type 'string'.
2+
Type 'number' is not assignable to type 'string'.
3+
dependentDestructuredVariablesWithDefaults.ts(85,15): error TS2322: Type 'string | number' is not assignable to type 'string'.
4+
Type 'number' is not assignable to type 'string'.
5+
dependentDestructuredVariablesWithDefaults.ts(96,15): error TS2322: Type 'string | number' is not assignable to type 'string'.
6+
Type 'number' is not assignable to type 'string'.
7+
8+
9+
==== dependentDestructuredVariablesWithDefaults.ts (3 errors) ====
10+
// https://github.com/microsoft/TypeScript/issues/50139
11+
// A default on a *required* discriminant must not disable discriminant narrowing of siblings.
12+
13+
type Props =
14+
| { isText: true, children: string }
15+
| { isText: false, children: number };
16+
17+
// Baseline: no default — narrows (already worked).
18+
function noDefault({ isText, children }: Props) {
19+
if (isText === true) {
20+
const s: string = children;
21+
} else {
22+
const n: number = children;
23+
}
24+
}
25+
26+
// The bug: default on a required discriminant should still narrow siblings.
27+
function withDefault({ isText = false, children }: Props) {
28+
if (isText === true) {
29+
const s: string = children;
30+
} else {
31+
const n: number = children;
32+
}
33+
}
34+
35+
// switch form.
36+
function withDefaultSwitch({ isText = false, children }: Props) {
37+
switch (isText) {
38+
case true: { const s: string = children; break; }
39+
case false: { const n: number = children; break; }
40+
}
41+
}
42+
43+
// Renamed binding `{ isText: t = false }`.
44+
function renamed({ isText: t = false, children }: Props) {
45+
if (t === true) {
46+
const s: string = children;
47+
} else {
48+
const n: number = children;
49+
}
50+
}
51+
52+
// Three-way union with a string discriminant.
53+
type Three =
54+
| { t: "i", v: number }
55+
| { t: "s", v: string }
56+
| { t: "b", v: boolean };
57+
58+
function threeWay({ t = "i", v }: Three) {
59+
if (t === "s") {
60+
const s: string = v;
61+
}
62+
}
63+
64+
// const destructuring (not a parameter).
65+
declare const props: Props;
66+
function constDestructure() {
67+
const { isText = false, children } = props;
68+
if (isText === true) {
69+
const s: string = children;
70+
}
71+
}
72+
73+
// --- Soundness boundary: these must still error ---
74+
75+
// Optional discriminant + default: calling with the property omitted yields the default,
76+
// so the sibling cannot be safely narrowed.
77+
type OptDisc =
78+
| { kind?: "a", x: string }
79+
| { kind?: "b", x: number };
80+
81+
function optionalDiscriminant({ kind = "a", x }: OptDisc) {
82+
if (kind === "a") {
83+
const s: string = x; // error
84+
~
85+
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
86+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
87+
}
88+
}
89+
90+
// Discriminant whose type already includes undefined.
91+
type UndefDisc =
92+
| { kind: "a" | undefined, x: string }
93+
| { kind: "b", x: number };
94+
95+
function undefinedDiscriminant({ kind = "a", x }: UndefDisc) {
96+
if (kind === "a") {
97+
const s: string = x; // error
98+
~
99+
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
100+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
101+
}
102+
}
103+
104+
// Sibling has its OWN default: it must not be narrowed from the parent.
105+
type PropsOpt =
106+
| { isText: true, children?: string }
107+
| { isText: false, children?: number };
108+
109+
function siblingDefault({ isText = false, children = 0 }: PropsOpt) {
110+
if (isText === true) {
111+
const s: string = children; // error
112+
~
113+
!!! error TS2322: Type 'string | number' is not assignable to type 'string'.
114+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
115+
}
116+
}
117+
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
//// [tests/cases/conformance/controlFlow/dependentDestructuredVariablesWithDefaults.ts] ////
2+
3+
=== dependentDestructuredVariablesWithDefaults.ts ===
4+
// https://github.com/microsoft/TypeScript/issues/50139
5+
// A default on a *required* discriminant must not disable discriminant narrowing of siblings.
6+
7+
type Props =
8+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
9+
10+
| { isText: true, children: string }
11+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 4, 7))
12+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 4, 21))
13+
14+
| { isText: false, children: number };
15+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 5, 7))
16+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 5, 22))
17+
18+
// Baseline: no default — narrows (already worked).
19+
function noDefault({ isText, children }: Props) {
20+
>noDefault : Symbol(noDefault, Decl(dependentDestructuredVariablesWithDefaults.ts, 5, 42))
21+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 8, 20))
22+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 8, 28))
23+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
24+
25+
if (isText === true) {
26+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 8, 20))
27+
28+
const s: string = children;
29+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 10, 13))
30+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 8, 28))
31+
32+
} else {
33+
const n: number = children;
34+
>n : Symbol(n, Decl(dependentDestructuredVariablesWithDefaults.ts, 12, 13))
35+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 8, 28))
36+
}
37+
}
38+
39+
// The bug: default on a required discriminant should still narrow siblings.
40+
function withDefault({ isText = false, children }: Props) {
41+
>withDefault : Symbol(withDefault, Decl(dependentDestructuredVariablesWithDefaults.ts, 14, 1))
42+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 17, 22))
43+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 17, 38))
44+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
45+
46+
if (isText === true) {
47+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 17, 22))
48+
49+
const s: string = children;
50+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 19, 13))
51+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 17, 38))
52+
53+
} else {
54+
const n: number = children;
55+
>n : Symbol(n, Decl(dependentDestructuredVariablesWithDefaults.ts, 21, 13))
56+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 17, 38))
57+
}
58+
}
59+
60+
// switch form.
61+
function withDefaultSwitch({ isText = false, children }: Props) {
62+
>withDefaultSwitch : Symbol(withDefaultSwitch, Decl(dependentDestructuredVariablesWithDefaults.ts, 23, 1))
63+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 26, 28))
64+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 26, 44))
65+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
66+
67+
switch (isText) {
68+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 26, 28))
69+
70+
case true: { const s: string = children; break; }
71+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 28, 26))
72+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 26, 44))
73+
74+
case false: { const n: number = children; break; }
75+
>n : Symbol(n, Decl(dependentDestructuredVariablesWithDefaults.ts, 29, 27))
76+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 26, 44))
77+
}
78+
}
79+
80+
// Renamed binding `{ isText: t = false }`.
81+
function renamed({ isText: t = false, children }: Props) {
82+
>renamed : Symbol(renamed, Decl(dependentDestructuredVariablesWithDefaults.ts, 31, 1))
83+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 4, 7), Decl(dependentDestructuredVariablesWithDefaults.ts, 5, 7))
84+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 34, 18))
85+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 34, 37))
86+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
87+
88+
if (t === true) {
89+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 34, 18))
90+
91+
const s: string = children;
92+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 36, 13))
93+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 34, 37))
94+
95+
} else {
96+
const n: number = children;
97+
>n : Symbol(n, Decl(dependentDestructuredVariablesWithDefaults.ts, 38, 13))
98+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 34, 37))
99+
}
100+
}
101+
102+
// Three-way union with a string discriminant.
103+
type Three =
104+
>Three : Symbol(Three, Decl(dependentDestructuredVariablesWithDefaults.ts, 40, 1))
105+
106+
| { t: "i", v: number }
107+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 44, 7))
108+
>v : Symbol(v, Decl(dependentDestructuredVariablesWithDefaults.ts, 44, 15))
109+
110+
| { t: "s", v: string }
111+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 45, 7))
112+
>v : Symbol(v, Decl(dependentDestructuredVariablesWithDefaults.ts, 45, 15))
113+
114+
| { t: "b", v: boolean };
115+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 46, 7))
116+
>v : Symbol(v, Decl(dependentDestructuredVariablesWithDefaults.ts, 46, 15))
117+
118+
function threeWay({ t = "i", v }: Three) {
119+
>threeWay : Symbol(threeWay, Decl(dependentDestructuredVariablesWithDefaults.ts, 46, 29))
120+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 48, 19))
121+
>v : Symbol(v, Decl(dependentDestructuredVariablesWithDefaults.ts, 48, 28))
122+
>Three : Symbol(Three, Decl(dependentDestructuredVariablesWithDefaults.ts, 40, 1))
123+
124+
if (t === "s") {
125+
>t : Symbol(t, Decl(dependentDestructuredVariablesWithDefaults.ts, 48, 19))
126+
127+
const s: string = v;
128+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 50, 13))
129+
>v : Symbol(v, Decl(dependentDestructuredVariablesWithDefaults.ts, 48, 28))
130+
}
131+
}
132+
133+
// const destructuring (not a parameter).
134+
declare const props: Props;
135+
>props : Symbol(props, Decl(dependentDestructuredVariablesWithDefaults.ts, 55, 13))
136+
>Props : Symbol(Props, Decl(dependentDestructuredVariablesWithDefaults.ts, 0, 0))
137+
138+
function constDestructure() {
139+
>constDestructure : Symbol(constDestructure, Decl(dependentDestructuredVariablesWithDefaults.ts, 55, 27))
140+
141+
const { isText = false, children } = props;
142+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 57, 11))
143+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 57, 27))
144+
>props : Symbol(props, Decl(dependentDestructuredVariablesWithDefaults.ts, 55, 13))
145+
146+
if (isText === true) {
147+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 57, 11))
148+
149+
const s: string = children;
150+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 59, 13))
151+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 57, 27))
152+
}
153+
}
154+
155+
// --- Soundness boundary: these must still error ---
156+
157+
// Optional discriminant + default: calling with the property omitted yields the default,
158+
// so the sibling cannot be safely narrowed.
159+
type OptDisc =
160+
>OptDisc : Symbol(OptDisc, Decl(dependentDestructuredVariablesWithDefaults.ts, 61, 1))
161+
162+
| { kind?: "a", x: string }
163+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 68, 7))
164+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 68, 19))
165+
166+
| { kind?: "b", x: number };
167+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 69, 7))
168+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 69, 19))
169+
170+
function optionalDiscriminant({ kind = "a", x }: OptDisc) {
171+
>optionalDiscriminant : Symbol(optionalDiscriminant, Decl(dependentDestructuredVariablesWithDefaults.ts, 69, 32))
172+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 71, 31))
173+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 71, 43))
174+
>OptDisc : Symbol(OptDisc, Decl(dependentDestructuredVariablesWithDefaults.ts, 61, 1))
175+
176+
if (kind === "a") {
177+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 71, 31))
178+
179+
const s: string = x; // error
180+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 73, 13))
181+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 71, 43))
182+
}
183+
}
184+
185+
// Discriminant whose type already includes undefined.
186+
type UndefDisc =
187+
>UndefDisc : Symbol(UndefDisc, Decl(dependentDestructuredVariablesWithDefaults.ts, 75, 1))
188+
189+
| { kind: "a" | undefined, x: string }
190+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 79, 7))
191+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 79, 30))
192+
193+
| { kind: "b", x: number };
194+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 80, 7))
195+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 80, 18))
196+
197+
function undefinedDiscriminant({ kind = "a", x }: UndefDisc) {
198+
>undefinedDiscriminant : Symbol(undefinedDiscriminant, Decl(dependentDestructuredVariablesWithDefaults.ts, 80, 31))
199+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 82, 32))
200+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 82, 44))
201+
>UndefDisc : Symbol(UndefDisc, Decl(dependentDestructuredVariablesWithDefaults.ts, 75, 1))
202+
203+
if (kind === "a") {
204+
>kind : Symbol(kind, Decl(dependentDestructuredVariablesWithDefaults.ts, 82, 32))
205+
206+
const s: string = x; // error
207+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 84, 13))
208+
>x : Symbol(x, Decl(dependentDestructuredVariablesWithDefaults.ts, 82, 44))
209+
}
210+
}
211+
212+
// Sibling has its OWN default: it must not be narrowed from the parent.
213+
type PropsOpt =
214+
>PropsOpt : Symbol(PropsOpt, Decl(dependentDestructuredVariablesWithDefaults.ts, 86, 1))
215+
216+
| { isText: true, children?: string }
217+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 90, 7))
218+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 90, 21))
219+
220+
| { isText: false, children?: number };
221+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 91, 7))
222+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 91, 22))
223+
224+
function siblingDefault({ isText = false, children = 0 }: PropsOpt) {
225+
>siblingDefault : Symbol(siblingDefault, Decl(dependentDestructuredVariablesWithDefaults.ts, 91, 43))
226+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 93, 25))
227+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 93, 41))
228+
>PropsOpt : Symbol(PropsOpt, Decl(dependentDestructuredVariablesWithDefaults.ts, 86, 1))
229+
230+
if (isText === true) {
231+
>isText : Symbol(isText, Decl(dependentDestructuredVariablesWithDefaults.ts, 93, 25))
232+
233+
const s: string = children; // error
234+
>s : Symbol(s, Decl(dependentDestructuredVariablesWithDefaults.ts, 95, 13))
235+
>children : Symbol(children, Decl(dependentDestructuredVariablesWithDefaults.ts, 93, 41))
236+
}
237+
}
238+

0 commit comments

Comments
 (0)