From 278f1727aa3f21188ef5d7f420a32446c734245e Mon Sep 17 00:00:00 2001 From: Oleksandr T Date: Sun, 4 Jul 2021 21:20:37 +0300 Subject: [PATCH] feat(42639): allow narrowing type in 'in' operator with the identifier on the left side --- src/compiler/binder.ts | 2 +- src/compiler/checker.ts | 13 ++-- .../reference/controlFlowInOperator.js | 47 ++++++++++++ .../reference/controlFlowInOperator.symbols | 63 ++++++++++++++++ .../reference/controlFlowInOperator.types | 72 +++++++++++++++++++ .../controlFlow/controlFlowInOperator.ts | 26 +++++++ 6 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 tests/baselines/reference/controlFlowInOperator.js create mode 100644 tests/baselines/reference/controlFlowInOperator.symbols create mode 100644 tests/baselines/reference/controlFlowInOperator.types create mode 100644 tests/cases/conformance/controlFlow/controlFlowInOperator.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index faefba86582f1..5ba3999e1a82d 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -917,7 +917,7 @@ namespace ts { } function isNarrowableInOperands(left: Expression, right: Expression) { - return isStringLiteralLike(left) && isNarrowingExpression(right); + return isNarrowingExpression(right) && (isIdentifier(left) || isStringLiteralLike(left)); } function isNarrowingBinaryExpression(expr: BinaryExpression) { diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d8d282086fdc4..55bd60639ac60 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -23562,13 +23562,12 @@ namespace ts { return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue; } - function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) { + function narrowByInKeyword(type: Type, name: __String, assumeTrue: boolean) { if (type.flags & TypeFlags.Union || type.flags & TypeFlags.Object && declaredType !== type || isThisTypeParameter(type) || type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol)) { - const propName = escapeLeadingUnderscores(literal.text); - return filterType(type, t => isTypePresencePossible(t, propName, assumeTrue)); + return filterType(type, t => isTypePresencePossible(t, name, assumeTrue)); } return type; } @@ -23626,13 +23625,15 @@ namespace ts { return narrowTypeByInstanceof(type, expr, assumeTrue); case SyntaxKind.InKeyword: const target = getReferenceCandidate(expr.right); - if (isStringLiteralLike(expr.left)) { + const leftType = getTypeOfNode(expr.left); + if (leftType.flags & TypeFlags.StringLiteral) { + const name = escapeLeadingUnderscores((leftType as StringLiteralType).value); if (containsMissingType(type) && isAccessExpression(reference) && isMatchingReference(reference.expression, target) && - getAccessedPropertyName(reference) === escapeLeadingUnderscores(expr.left.text)) { + getAccessedPropertyName(reference) === name) { return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined); } if (isMatchingReference(reference, target)) { - return narrowByInKeyword(type, expr.left, assumeTrue); + return narrowByInKeyword(type, name, assumeTrue); } } break; diff --git a/tests/baselines/reference/controlFlowInOperator.js b/tests/baselines/reference/controlFlowInOperator.js new file mode 100644 index 0000000000000..d4a5731eb9844 --- /dev/null +++ b/tests/baselines/reference/controlFlowInOperator.js @@ -0,0 +1,47 @@ +//// [controlFlowInOperator.ts] +const a = 'a'; +const b = 'b'; +const d = 'd'; + +type A = { [a]: number; }; +type B = { [b]: string; }; + +declare const c: A | B; + +if ('a' in c) { + c; // A + c['a']; // number; +} + +if ('d' in c) { + c; // never +} + +if (a in c) { + c; // A + c[a]; // number; +} + +if (d in c) { + c; // never +} + + +//// [controlFlowInOperator.js] +var a = 'a'; +var b = 'b'; +var d = 'd'; +if ('a' in c) { + c; // A + c['a']; // number; +} +if ('d' in c) { + c; // never +} +if (a in c) { + c; // A + c[a]; // number; +} +if (d in c) { + c; // never +} diff --git a/tests/baselines/reference/controlFlowInOperator.symbols b/tests/baselines/reference/controlFlowInOperator.symbols new file mode 100644 index 0000000000000..03b0e4b40b4b5 --- /dev/null +++ b/tests/baselines/reference/controlFlowInOperator.symbols @@ -0,0 +1,63 @@ +=== tests/cases/conformance/controlFlow/controlFlowInOperator.ts === +const a = 'a'; +>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5)) + +const b = 'b'; +>b : Symbol(b, Decl(controlFlowInOperator.ts, 1, 5)) + +const d = 'd'; +>d : Symbol(d, Decl(controlFlowInOperator.ts, 2, 5)) + +type A = { [a]: number; }; +>A : Symbol(A, Decl(controlFlowInOperator.ts, 2, 14)) +>[a] : Symbol([a], Decl(controlFlowInOperator.ts, 4, 10)) +>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5)) + +type B = { [b]: string; }; +>B : Symbol(B, Decl(controlFlowInOperator.ts, 4, 26)) +>[b] : Symbol([b], Decl(controlFlowInOperator.ts, 5, 10)) +>b : Symbol(b, Decl(controlFlowInOperator.ts, 1, 5)) + +declare const c: A | B; +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) +>A : Symbol(A, Decl(controlFlowInOperator.ts, 2, 14)) +>B : Symbol(B, Decl(controlFlowInOperator.ts, 4, 26)) + +if ('a' in c) { +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c; // A +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c['a']; // number; +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) +>'a' : Symbol([a], Decl(controlFlowInOperator.ts, 4, 10)) +} + +if ('d' in c) { +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c; // never +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) +} + +if (a in c) { +>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5)) +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c; // A +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c[a]; // number; +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) +>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5)) +} + +if (d in c) { +>d : Symbol(d, Decl(controlFlowInOperator.ts, 2, 5)) +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) + + c; // never +>c : Symbol(c, Decl(controlFlowInOperator.ts, 7, 13)) +} + diff --git a/tests/baselines/reference/controlFlowInOperator.types b/tests/baselines/reference/controlFlowInOperator.types new file mode 100644 index 0000000000000..cae50e4052f8e --- /dev/null +++ b/tests/baselines/reference/controlFlowInOperator.types @@ -0,0 +1,72 @@ +=== tests/cases/conformance/controlFlow/controlFlowInOperator.ts === +const a = 'a'; +>a : "a" +>'a' : "a" + +const b = 'b'; +>b : "b" +>'b' : "b" + +const d = 'd'; +>d : "d" +>'d' : "d" + +type A = { [a]: number; }; +>A : A +>[a] : number +>a : "a" + +type B = { [b]: string; }; +>B : B +>[b] : string +>b : "b" + +declare const c: A | B; +>c : A | B + +if ('a' in c) { +>'a' in c : boolean +>'a' : "a" +>c : A | B + + c; // A +>c : A + + c['a']; // number; +>c['a'] : number +>c : A +>'a' : "a" +} + +if ('d' in c) { +>'d' in c : boolean +>'d' : "d" +>c : A | B + + c; // never +>c : never +} + +if (a in c) { +>a in c : boolean +>a : "a" +>c : A | B + + c; // A +>c : A + + c[a]; // number; +>c[a] : number +>c : A +>a : "a" +} + +if (d in c) { +>d in c : boolean +>d : "d" +>c : A | B + + c; // never +>c : never +} + diff --git a/tests/cases/conformance/controlFlow/controlFlowInOperator.ts b/tests/cases/conformance/controlFlow/controlFlowInOperator.ts new file mode 100644 index 0000000000000..5dc27c45e8692 --- /dev/null +++ b/tests/cases/conformance/controlFlow/controlFlowInOperator.ts @@ -0,0 +1,26 @@ +const a = 'a'; +const b = 'b'; +const d = 'd'; + +type A = { [a]: number; }; +type B = { [b]: string; }; + +declare const c: A | B; + +if ('a' in c) { + c; // A + c['a']; // number; +} + +if ('d' in c) { + c; // never +} + +if (a in c) { + c; // A + c[a]; // number; +} + +if (d in c) { + c; // never +}