Skip to content

Commit ecc93ee

Browse files
committed
feat(42639): allow narrowing type in 'in' operator with the identifier on the left side
1 parent b539c9a commit ecc93ee

File tree

6 files changed

+162
-7
lines changed

6 files changed

+162
-7
lines changed

src/compiler/binder.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ namespace ts {
917917
}
918918

919919
function isNarrowableInOperands(left: Expression, right: Expression) {
920-
return isStringLiteralLike(left) && isNarrowingExpression(right);
920+
return isNarrowingExpression(right) || (isIdentifier(left) || isStringLiteralLike(left));
921921
}
922922

923923
function isNarrowingBinaryExpression(expr: BinaryExpression) {

src/compiler/checker.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -23562,13 +23562,24 @@ namespace ts {
2356223562
return getApplicableIndexInfoForName(type, propName) ? true : !assumeTrue;
2356323563
}
2356423564

23565-
function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
23565+
23566+
function tryGetNarrowableInOperandName(node: Node): __String | undefined {
23567+
if (isStringLiteralLike(node)) {
23568+
return escapeLeadingUnderscores(node.text);
23569+
}
23570+
const decl = getNodeLinks(node).resolvedSymbol?.valueDeclaration;
23571+
if (decl && hasInitializer(decl) && decl.initializer) {
23572+
return tryGetNarrowableInOperandName(decl.initializer);
23573+
}
23574+
return undefined;
23575+
}
23576+
23577+
function narrowByInKeyword(type: Type, name: __String, assumeTrue: boolean) {
2356623578
if (type.flags & TypeFlags.Union
2356723579
|| type.flags & TypeFlags.Object && declaredType !== type
2356823580
|| isThisTypeParameter(type)
2356923581
|| type.flags & TypeFlags.Intersection && every((type as IntersectionType).types, t => t.symbol !== globalThisSymbol)) {
23570-
const propName = escapeLeadingUnderscores(literal.text);
23571-
return filterType(type, t => isTypePresencePossible(t, propName, assumeTrue));
23582+
return filterType(type, t => isTypePresencePossible(t, name, assumeTrue));
2357223583
}
2357323584
return type;
2357423585
}
@@ -23626,13 +23637,14 @@ namespace ts {
2362623637
return narrowTypeByInstanceof(type, expr, assumeTrue);
2362723638
case SyntaxKind.InKeyword:
2362823639
const target = getReferenceCandidate(expr.right);
23629-
if (isStringLiteralLike(expr.left)) {
23640+
const name = tryGetNarrowableInOperandName(expr.left);
23641+
if (name) {
2363023642
if (containsMissingType(type) && isAccessExpression(reference) && isMatchingReference(reference.expression, target) &&
23631-
getAccessedPropertyName(reference) === escapeLeadingUnderscores(expr.left.text)) {
23643+
getAccessedPropertyName(reference) === name) {
2363223644
return getTypeWithFacts(type, assumeTrue ? TypeFacts.NEUndefined : TypeFacts.EQUndefined);
2363323645
}
2363423646
if (isMatchingReference(reference, target)) {
23635-
return narrowByInKeyword(type, expr.left, assumeTrue);
23647+
return narrowByInKeyword(type, name, assumeTrue);
2363623648
}
2363723649
}
2363823650
break;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//// [controlFlowInOperator.ts]
2+
const a = 'a';
3+
const b = 'b';
4+
5+
type A = { [a]: number; };
6+
type B = { [b]: string; };
7+
8+
declare const c: A | B;
9+
10+
if ('a' in c) {
11+
c; // A
12+
c['a']; // number;
13+
}
14+
15+
if (a in c) {
16+
c; // A
17+
c[a]; // number;
18+
}
19+
20+
21+
//// [controlFlowInOperator.js]
22+
var a = 'a';
23+
var b = 'b';
24+
if ('a' in c) {
25+
c; // A
26+
c['a']; // number;
27+
}
28+
if (a in c) {
29+
c; // A
30+
c[a]; // number;
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
=== tests/cases/conformance/controlFlow/controlFlowInOperator.ts ===
2+
const a = 'a';
3+
>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5))
4+
5+
const b = 'b';
6+
>b : Symbol(b, Decl(controlFlowInOperator.ts, 1, 5))
7+
8+
type A = { [a]: number; };
9+
>A : Symbol(A, Decl(controlFlowInOperator.ts, 1, 14))
10+
>[a] : Symbol([a], Decl(controlFlowInOperator.ts, 3, 10))
11+
>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5))
12+
13+
type B = { [b]: string; };
14+
>B : Symbol(B, Decl(controlFlowInOperator.ts, 3, 26))
15+
>[b] : Symbol([b], Decl(controlFlowInOperator.ts, 4, 10))
16+
>b : Symbol(b, Decl(controlFlowInOperator.ts, 1, 5))
17+
18+
declare const c: A | B;
19+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
20+
>A : Symbol(A, Decl(controlFlowInOperator.ts, 1, 14))
21+
>B : Symbol(B, Decl(controlFlowInOperator.ts, 3, 26))
22+
23+
if ('a' in c) {
24+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
25+
26+
c; // A
27+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
28+
29+
c['a']; // number;
30+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
31+
>'a' : Symbol([a], Decl(controlFlowInOperator.ts, 3, 10))
32+
}
33+
34+
if (a in c) {
35+
>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5))
36+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
37+
38+
c; // A
39+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
40+
41+
c[a]; // number;
42+
>c : Symbol(c, Decl(controlFlowInOperator.ts, 6, 13))
43+
>a : Symbol(a, Decl(controlFlowInOperator.ts, 0, 5))
44+
}
45+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
=== tests/cases/conformance/controlFlow/controlFlowInOperator.ts ===
2+
const a = 'a';
3+
>a : "a"
4+
>'a' : "a"
5+
6+
const b = 'b';
7+
>b : "b"
8+
>'b' : "b"
9+
10+
type A = { [a]: number; };
11+
>A : A
12+
>[a] : number
13+
>a : "a"
14+
15+
type B = { [b]: string; };
16+
>B : B
17+
>[b] : string
18+
>b : "b"
19+
20+
declare const c: A | B;
21+
>c : A | B
22+
23+
if ('a' in c) {
24+
>'a' in c : boolean
25+
>'a' : "a"
26+
>c : A | B
27+
28+
c; // A
29+
>c : A
30+
31+
c['a']; // number;
32+
>c['a'] : number
33+
>c : A
34+
>'a' : "a"
35+
}
36+
37+
if (a in c) {
38+
>a in c : boolean
39+
>a : "a"
40+
>c : A | B
41+
42+
c; // A
43+
>c : A
44+
45+
c[a]; // number;
46+
>c[a] : number
47+
>c : A
48+
>a : "a"
49+
}
50+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const a = 'a';
2+
const b = 'b';
3+
4+
type A = { [a]: number; };
5+
type B = { [b]: string; };
6+
7+
declare const c: A | B;
8+
9+
if ('a' in c) {
10+
c; // A
11+
c['a']; // number;
12+
}
13+
14+
if (a in c) {
15+
c; // A
16+
c[a]; // number;
17+
}

0 commit comments

Comments
 (0)