diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 803c9f6070980..b35f7c0196d47 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2061,8 +2061,10 @@ namespace ts { case SyntaxKind.Parameter: return bindParameter(node); case SyntaxKind.VariableDeclaration: + return bindVariableDeclarationOrBindingElement(node); case SyntaxKind.BindingElement: - return bindVariableDeclarationOrBindingElement(node); + node.flowNode = currentFlow; + return bindVariableDeclarationOrBindingElement(node); case SyntaxKind.PropertyDeclaration: case SyntaxKind.PropertySignature: return bindPropertyWorker(node as PropertyDeclaration | PropertySignature); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index dafa75372e1bf..a0a41a3fb61fb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4088,8 +4088,8 @@ namespace ts { /** Return the inferred type for a binding element */ function getTypeForBindingElement(declaration: BindingElement): Type { - const pattern = declaration.parent; - const parentType = getTypeForBindingElementParent(pattern.parent); + const pattern = declaration.parent; + const parentType = getTypeForBindingElementParent(pattern.parent); // If parent has the unknown (error) type, then so does this binding element if (parentType === unknownType) { return unknownType; @@ -4134,7 +4134,8 @@ namespace ts { // or otherwise the type of the string index signature. const text = getTextOfPropertyName(name); - type = getTypeOfPropertyOfType(parentType, text) || + const declaredType = getTypeOfPropertyOfType(parentType, text); + type = declaredType && getFlowTypeOfReference(declaration, declaredType) || isNumericLiteralName(text) && getIndexTypeOfType(parentType, IndexKind.Number) || getIndexTypeOfType(parentType, IndexKind.String); if (!type) { @@ -10656,7 +10657,7 @@ namespace ts { // The result is undefined if the reference isn't a dotted name. We prefix nodes // occurring in an apparent type position with '@' because the control flow type // of such nodes may be based on the apparent type instead of the declared type. - function getFlowCacheKey(node: Node): string { + function getFlowCacheKey(node: Node): string | undefined { if (node.kind === SyntaxKind.Identifier) { const symbol = getResolvedSymbol(node); return symbol !== unknownSymbol ? (isApparentTypePosition(node) ? "@" : "") + getSymbolId(symbol) : undefined; @@ -10666,7 +10667,14 @@ namespace ts { } if (node.kind === SyntaxKind.PropertyAccessExpression) { const key = getFlowCacheKey((node).expression); - return key && key + "." + (node).name.text; + return key && key + "." + unescapeLeadingUnderscores((node).name.text); + } + if (node.kind === SyntaxKind.BindingElement) { + const container = (node as BindingElement).parent.parent; + const key = container.kind === SyntaxKind.BindingElement ? getFlowCacheKey(container) : (container.initializer && getFlowCacheKey(container.initializer)); + const text = getBindingElementNameText(node as BindingElement); + const result = key && text && (key + "." + text); + return result; } return undefined; } @@ -10682,6 +10690,28 @@ namespace ts { return undefined; } + function getBindingElementNameText(element: BindingElement): string | undefined { + if (element.parent.kind === SyntaxKind.ObjectBindingPattern) { + const name = element.propertyName || element.name; + switch (name.kind) { + case SyntaxKind.Identifier: + return unescapeLeadingUnderscores(name.text); + case SyntaxKind.ComputedPropertyName: + if (isComputedNonLiteralName(name as PropertyName)) return undefined; + return (name.expression as LiteralExpression).text; + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + return name.text; + default: + // Per types, array and object binding patterns remain, however they should never be present if propertyName is not defined + Debug.fail("Unexpected name kind for binding element name"); + } + } + else { + return "" + element.parent.elements.indexOf(element); + } + } + function isMatchingReference(source: Node, target: Node): boolean { switch (source.kind) { case SyntaxKind.Identifier: @@ -10696,6 +10726,17 @@ namespace ts { return target.kind === SyntaxKind.PropertyAccessExpression && (source).name.text === (target).name.text && isMatchingReference((source).expression, (target).expression); + case SyntaxKind.BindingElement: + if (target.kind !== SyntaxKind.PropertyAccessExpression) return false; + const t = target as PropertyAccessExpression; + if (t.name.text !== getBindingElementNameText(source as BindingElement)) return false; + if (source.parent.parent.kind === SyntaxKind.BindingElement && isMatchingReference(source.parent.parent, t.expression)) { + return true; + } + if (source.parent.parent.kind === SyntaxKind.VariableDeclaration) { + const maybeId = (source.parent.parent as VariableDeclaration).initializer; + return maybeId && isMatchingReference(maybeId, t.expression); + } } return false; } @@ -11486,6 +11527,10 @@ namespace ts { const cache = flowLoopCaches[id] || (flowLoopCaches[id] = createMap()); if (!key) { key = getFlowCacheKey(reference); + // No cache key is generated when binding patterns are in unnarrowable situations + if (!key) { + return declaredType; + } } const cached = cache.get(key); if (cached) { diff --git a/tests/baselines/reference/destructuringTypeGuardFlow.js b/tests/baselines/reference/destructuringTypeGuardFlow.js new file mode 100644 index 0000000000000..15997de7b44b5 --- /dev/null +++ b/tests/baselines/reference/destructuringTypeGuardFlow.js @@ -0,0 +1,57 @@ +//// [destructuringTypeGuardFlow.ts] +type foo = { + bar: number | null; + baz: string; + nested: { + a: number; + b: string | null; + } +}; + +const aFoo: foo = { bar: 3, baz: "b", nested: { a: 1, b: "y" } }; + +if (aFoo.bar && aFoo.nested.b) { + const { bar, baz, nested: {a, b: text} } = aFoo; + const right: number = aFoo.bar; + const wrong: number = bar; + const another: string = baz; + const aAgain: number = a; + const bAgain: string = text; +} + +type bar = { + elem1: number | null; + elem2: foo | null; +}; + +const bBar = { elem1: 7, elem2: aFoo }; + +if (bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b) { + const { bar, baz, nested: {a, b: text} } = bBar.elem2; + const right: number = bBar.elem2.bar; + const wrong: number = bar; + const another: string = baz; + const aAgain: number = a; + const bAgain: string = text; +} + + +//// [destructuringTypeGuardFlow.js] +var aFoo = { bar: 3, baz: "b", nested: { a: 1, b: "y" } }; +if (aFoo.bar && aFoo.nested.b) { + var bar = aFoo.bar, baz = aFoo.baz, _a = aFoo.nested, a = _a.a, text = _a.b; + var right = aFoo.bar; + var wrong = bar; + var another = baz; + var aAgain = a; + var bAgain = text; +} +var bBar = { elem1: 7, elem2: aFoo }; +if (bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b) { + var _b = bBar.elem2, bar = _b.bar, baz = _b.baz, _c = _b.nested, a = _c.a, text = _c.b; + var right = bBar.elem2.bar; + var wrong = bar; + var another = baz; + var aAgain = a; + var bAgain = text; +} diff --git a/tests/baselines/reference/destructuringTypeGuardFlow.symbols b/tests/baselines/reference/destructuringTypeGuardFlow.symbols new file mode 100644 index 0000000000000..4c601acb0ae79 --- /dev/null +++ b/tests/baselines/reference/destructuringTypeGuardFlow.symbols @@ -0,0 +1,143 @@ +=== tests/cases/compiler/destructuringTypeGuardFlow.ts === +type foo = { +>foo : Symbol(foo, Decl(destructuringTypeGuardFlow.ts, 0, 0)) + + bar: number | null; +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) + + baz: string; +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 1, 21)) + + nested: { +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) + + a: number; +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 3, 11)) + + b: string | null; +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) + } +}; + +const aFoo: foo = { bar: 3, baz: "b", nested: { a: 1, b: "y" } }; +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) +>foo : Symbol(foo, Decl(destructuringTypeGuardFlow.ts, 0, 0)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 9, 19)) +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 9, 27)) +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 9, 37)) +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 9, 47)) +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 9, 53)) + +if (aFoo.bar && aFoo.nested.b) { +>aFoo.bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>aFoo.nested.b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) +>aFoo.nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) + + const { bar, baz, nested: {a, b: text} } = aFoo; +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 12, 9)) +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 12, 14)) +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 12, 29)) +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) +>text : Symbol(text, Decl(destructuringTypeGuardFlow.ts, 12, 31)) +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) + + const right: number = aFoo.bar; +>right : Symbol(right, Decl(destructuringTypeGuardFlow.ts, 13, 7)) +>aFoo.bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) + + const wrong: number = bar; +>wrong : Symbol(wrong, Decl(destructuringTypeGuardFlow.ts, 14, 7)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 12, 9)) + + const another: string = baz; +>another : Symbol(another, Decl(destructuringTypeGuardFlow.ts, 15, 7)) +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 12, 14)) + + const aAgain: number = a; +>aAgain : Symbol(aAgain, Decl(destructuringTypeGuardFlow.ts, 16, 7)) +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 12, 29)) + + const bAgain: string = text; +>bAgain : Symbol(bAgain, Decl(destructuringTypeGuardFlow.ts, 17, 7)) +>text : Symbol(text, Decl(destructuringTypeGuardFlow.ts, 12, 31)) +} + +type bar = { +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 18, 1)) + + elem1: number | null; +>elem1 : Symbol(elem1, Decl(destructuringTypeGuardFlow.ts, 20, 12)) + + elem2: foo | null; +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 21, 23)) +>foo : Symbol(foo, Decl(destructuringTypeGuardFlow.ts, 0, 0)) + +}; + +const bBar = { elem1: 7, elem2: aFoo }; +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem1 : Symbol(elem1, Decl(destructuringTypeGuardFlow.ts, 25, 14)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>aFoo : Symbol(aFoo, Decl(destructuringTypeGuardFlow.ts, 9, 5)) + +if (bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b) { +>bBar.elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar.elem2.bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>bBar.elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>bBar.elem2.nested.b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) +>bBar.elem2.nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>bBar.elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) + + const { bar, baz, nested: {a, b: text} } = bBar.elem2; +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 28, 9)) +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 28, 14)) +>nested : Symbol(nested, Decl(destructuringTypeGuardFlow.ts, 2, 14)) +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 28, 29)) +>b : Symbol(b, Decl(destructuringTypeGuardFlow.ts, 4, 14)) +>text : Symbol(text, Decl(destructuringTypeGuardFlow.ts, 28, 31)) +>bBar.elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) + + const right: number = bBar.elem2.bar; +>right : Symbol(right, Decl(destructuringTypeGuardFlow.ts, 29, 7)) +>bBar.elem2.bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) +>bBar.elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bBar : Symbol(bBar, Decl(destructuringTypeGuardFlow.ts, 25, 5)) +>elem2 : Symbol(elem2, Decl(destructuringTypeGuardFlow.ts, 25, 24)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 0, 12)) + + const wrong: number = bar; +>wrong : Symbol(wrong, Decl(destructuringTypeGuardFlow.ts, 30, 7)) +>bar : Symbol(bar, Decl(destructuringTypeGuardFlow.ts, 28, 9)) + + const another: string = baz; +>another : Symbol(another, Decl(destructuringTypeGuardFlow.ts, 31, 7)) +>baz : Symbol(baz, Decl(destructuringTypeGuardFlow.ts, 28, 14)) + + const aAgain: number = a; +>aAgain : Symbol(aAgain, Decl(destructuringTypeGuardFlow.ts, 32, 7)) +>a : Symbol(a, Decl(destructuringTypeGuardFlow.ts, 28, 29)) + + const bAgain: string = text; +>bAgain : Symbol(bAgain, Decl(destructuringTypeGuardFlow.ts, 33, 7)) +>text : Symbol(text, Decl(destructuringTypeGuardFlow.ts, 28, 31)) +} + diff --git a/tests/baselines/reference/destructuringTypeGuardFlow.types b/tests/baselines/reference/destructuringTypeGuardFlow.types new file mode 100644 index 0000000000000..e04dfa645c9ca --- /dev/null +++ b/tests/baselines/reference/destructuringTypeGuardFlow.types @@ -0,0 +1,158 @@ +=== tests/cases/compiler/destructuringTypeGuardFlow.ts === +type foo = { +>foo : foo + + bar: number | null; +>bar : number | null +>null : null + + baz: string; +>baz : string + + nested: { +>nested : { a: number; b: string | null; } + + a: number; +>a : number + + b: string | null; +>b : string | null +>null : null + } +}; + +const aFoo: foo = { bar: 3, baz: "b", nested: { a: 1, b: "y" } }; +>aFoo : foo +>foo : foo +>{ bar: 3, baz: "b", nested: { a: 1, b: "y" } } : { bar: number; baz: string; nested: { a: number; b: string; }; } +>bar : number +>3 : 3 +>baz : string +>"b" : "b" +>nested : { a: number; b: string; } +>{ a: 1, b: "y" } : { a: number; b: string; } +>a : number +>1 : 1 +>b : string +>"y" : "y" + +if (aFoo.bar && aFoo.nested.b) { +>aFoo.bar && aFoo.nested.b : string | 0 | null +>aFoo.bar : number | null +>aFoo : foo +>bar : number | null +>aFoo.nested.b : string | null +>aFoo.nested : { a: number; b: string | null; } +>aFoo : foo +>nested : { a: number; b: string | null; } +>b : string | null + + const { bar, baz, nested: {a, b: text} } = aFoo; +>bar : number +>baz : string +>nested : any +>a : number +>b : any +>text : string +>aFoo : foo + + const right: number = aFoo.bar; +>right : number +>aFoo.bar : number +>aFoo : foo +>bar : number + + const wrong: number = bar; +>wrong : number +>bar : number + + const another: string = baz; +>another : string +>baz : string + + const aAgain: number = a; +>aAgain : number +>a : number + + const bAgain: string = text; +>bAgain : string +>text : string +} + +type bar = { +>bar : bar + + elem1: number | null; +>elem1 : number | null +>null : null + + elem2: foo | null; +>elem2 : foo | null +>foo : foo +>null : null + +}; + +const bBar = { elem1: 7, elem2: aFoo }; +>bBar : { elem1: number; elem2: foo; } +>{ elem1: 7, elem2: aFoo } : { elem1: number; elem2: foo; } +>elem1 : number +>7 : 7 +>elem2 : foo +>aFoo : foo + +if (bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b) { +>bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b : string | 0 | null +>bBar.elem2 && bBar.elem2.bar : number | null +>bBar.elem2 : foo +>bBar : { elem1: number; elem2: foo; } +>elem2 : foo +>bBar.elem2.bar : number | null +>bBar.elem2 : foo +>bBar : { elem1: number; elem2: foo; } +>elem2 : foo +>bar : number | null +>bBar.elem2.nested.b : string | null +>bBar.elem2.nested : { a: number; b: string | null; } +>bBar.elem2 : foo +>bBar : { elem1: number; elem2: foo; } +>elem2 : foo +>nested : { a: number; b: string | null; } +>b : string | null + + const { bar, baz, nested: {a, b: text} } = bBar.elem2; +>bar : number +>baz : string +>nested : any +>a : number +>b : any +>text : string +>bBar.elem2 : foo +>bBar : { elem1: number; elem2: foo; } +>elem2 : foo + + const right: number = bBar.elem2.bar; +>right : number +>bBar.elem2.bar : number +>bBar.elem2 : foo +>bBar : { elem1: number; elem2: foo; } +>elem2 : foo +>bar : number + + const wrong: number = bar; +>wrong : number +>bar : number + + const another: string = baz; +>another : string +>baz : string + + const aAgain: number = a; +>aAgain : number +>a : number + + const bAgain: string = text; +>bAgain : string +>text : string +} + diff --git a/tests/cases/compiler/destructuringTypeGuardFlow.ts b/tests/cases/compiler/destructuringTypeGuardFlow.ts new file mode 100644 index 0000000000000..b0a0b18948d1d --- /dev/null +++ b/tests/cases/compiler/destructuringTypeGuardFlow.ts @@ -0,0 +1,36 @@ +// @strictNullChecks: true +type foo = { + bar: number | null; + baz: string; + nested: { + a: number; + b: string | null; + } +}; + +const aFoo: foo = { bar: 3, baz: "b", nested: { a: 1, b: "y" } }; + +if (aFoo.bar && aFoo.nested.b) { + const { bar, baz, nested: {a, b: text} } = aFoo; + const right: number = aFoo.bar; + const wrong: number = bar; + const another: string = baz; + const aAgain: number = a; + const bAgain: string = text; +} + +type bar = { + elem1: number | null; + elem2: foo | null; +}; + +const bBar = { elem1: 7, elem2: aFoo }; + +if (bBar.elem2 && bBar.elem2.bar && bBar.elem2.nested.b) { + const { bar, baz, nested: {a, b: text} } = bBar.elem2; + const right: number = bBar.elem2.bar; + const wrong: number = bar; + const another: string = baz; + const aAgain: number = a; + const bAgain: string = text; +}