Skip to content

Commit 9c3c2ad

Browse files
committed
Add initial support for 'in' typeguarding
1 parent 7bb5fc2 commit 9c3c2ad

File tree

9 files changed

+1287
-0
lines changed

9 files changed

+1287
-0
lines changed

src/compiler/binder.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,10 @@ namespace ts {
749749
return expr1.kind === SyntaxKind.TypeOfExpression && isNarrowableOperand((<TypeOfExpression>expr1).expression) && expr2.kind === SyntaxKind.StringLiteral;
750750
}
751751

752+
function isNarrowableInOperands(left: Expression, right: Expression) {
753+
return left.kind === SyntaxKind.StringLiteral && isNarrowingExpression(right);
754+
}
755+
752756
function isNarrowingBinaryExpression(expr: BinaryExpression) {
753757
switch (expr.operatorToken.kind) {
754758
case SyntaxKind.EqualsToken:
@@ -761,6 +765,8 @@ namespace ts {
761765
isNarrowingTypeofOperands(expr.right, expr.left) || isNarrowingTypeofOperands(expr.left, expr.right);
762766
case SyntaxKind.InstanceOfKeyword:
763767
return isNarrowableOperand(expr.left);
768+
case SyntaxKind.InKeyword:
769+
return isNarrowableInOperands(expr.left, expr.right);
764770
case SyntaxKind.CommaToken:
765771
return isNarrowingExpression(expr.right);
766772
}

src/compiler/checker.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12722,6 +12722,14 @@ namespace ts {
1272212722
return type;
1272312723
}
1272412724

12725+
function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
12726+
if ((type.flags & (TypeFlags.Union | TypeFlags.Object)) || (type.flags & TypeFlags.TypeParameter && (type as TypeParameter).isThisType)) {
12727+
const propName = literal.text;
12728+
return filterType(type, t => !!getPropertyOfType(t, propName) === assumeTrue);
12729+
}
12730+
return type;
12731+
}
12732+
1272512733
function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1272612734
switch (expr.operatorToken.kind) {
1272712735
case SyntaxKind.EqualsToken:
@@ -12757,6 +12765,12 @@ namespace ts {
1275712765
break;
1275812766
case SyntaxKind.InstanceOfKeyword:
1275912767
return narrowTypeByInstanceof(type, expr, assumeTrue);
12768+
case SyntaxKind.InKeyword:
12769+
const target = getReferenceCandidate(expr.right);
12770+
if (expr.left.kind === SyntaxKind.StringLiteral && isMatchingReference(reference, target)) {
12771+
return narrowByInKeyword(type, <LiteralExpression>expr.left, assumeTrue);
12772+
}
12773+
break;
1276012774
case SyntaxKind.CommaToken:
1276112775
return narrowType(type, expr.right, assumeTrue);
1276212776
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
tests/cases/compiler/inKeywordTypeguard.ts(6,11): error TS2339: Property 'b' does not exist on type 'A'.
2+
tests/cases/compiler/inKeywordTypeguard.ts(8,11): error TS2339: Property 'a' does not exist on type 'B'.
3+
tests/cases/compiler/inKeywordTypeguard.ts(14,11): error TS2339: Property 'b' does not exist on type 'A'.
4+
tests/cases/compiler/inKeywordTypeguard.ts(16,11): error TS2339: Property 'a' does not exist on type 'B'.
5+
tests/cases/compiler/inKeywordTypeguard.ts(42,11): error TS2339: Property 'b' does not exist on type 'AWithMethod'.
6+
tests/cases/compiler/inKeywordTypeguard.ts(49,11): error TS2339: Property 'a' does not exist on type 'never'.
7+
tests/cases/compiler/inKeywordTypeguard.ts(50,11): error TS2339: Property 'b' does not exist on type 'never'.
8+
tests/cases/compiler/inKeywordTypeguard.ts(52,11): error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
9+
Property 'a' does not exist on type 'BWithMethod'.
10+
tests/cases/compiler/inKeywordTypeguard.ts(53,11): error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
11+
Property 'b' does not exist on type 'AWithMethod'.
12+
tests/cases/compiler/inKeywordTypeguard.ts(62,11): error TS2339: Property 'b' does not exist on type 'A | C | D'.
13+
Property 'b' does not exist on type 'A'.
14+
tests/cases/compiler/inKeywordTypeguard.ts(64,11): error TS2339: Property 'a' does not exist on type 'B'.
15+
tests/cases/compiler/inKeywordTypeguard.ts(72,32): error TS2339: Property 'b' does not exist on type 'A'.
16+
tests/cases/compiler/inKeywordTypeguard.ts(74,32): error TS2339: Property 'a' does not exist on type 'B'.
17+
tests/cases/compiler/inKeywordTypeguard.ts(82,39): error TS2339: Property 'b' does not exist on type 'A'.
18+
tests/cases/compiler/inKeywordTypeguard.ts(84,39): error TS2339: Property 'a' does not exist on type 'B'.
19+
tests/cases/compiler/inKeywordTypeguard.ts(94,26): error TS2339: Property 'a' does not exist on type 'never'.
20+
21+
22+
==== tests/cases/compiler/inKeywordTypeguard.ts (16 errors) ====
23+
class A { a: string; }
24+
class B { b: string; }
25+
26+
function negativeClassesTest(x: A | B) {
27+
if ("a" in x) {
28+
x.b = "1";
29+
~
30+
!!! error TS2339: Property 'b' does not exist on type 'A'.
31+
} else {
32+
x.a = "1";
33+
~
34+
!!! error TS2339: Property 'a' does not exist on type 'B'.
35+
}
36+
}
37+
38+
function positiveClassesTest(x: A | B) {
39+
if ("a" in x) {
40+
x.b = "1";
41+
~
42+
!!! error TS2339: Property 'b' does not exist on type 'A'.
43+
} else {
44+
x.a = "1";
45+
~
46+
!!! error TS2339: Property 'a' does not exist on type 'B'.
47+
}
48+
}
49+
50+
class AOpt { a?: string }
51+
class BOpn { b?: string }
52+
53+
function positiveTestClassesWithOptionalProperties(x: AOpt | BOpn) {
54+
if ("a" in x) {
55+
x.a = "1";
56+
} else {
57+
x.b = "1";
58+
}
59+
}
60+
61+
class AWithMethod {
62+
a(): string { return "" }
63+
}
64+
65+
class BWithMethod {
66+
b(): string { return "" }
67+
}
68+
69+
function negativeTestClassesWithMembers(x: AWithMethod | BWithMethod) {
70+
if ("a" in x) {
71+
x.a();
72+
x.b();
73+
~
74+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod'.
75+
} else {
76+
}
77+
}
78+
79+
function negativeTestClassesWithMemberMissingInBothClasses(x: AWithMethod | BWithMethod) {
80+
if ("c" in x) {
81+
x.a();
82+
~
83+
!!! error TS2339: Property 'a' does not exist on type 'never'.
84+
x.b();
85+
~
86+
!!! error TS2339: Property 'b' does not exist on type 'never'.
87+
} else {
88+
x.a();
89+
~
90+
!!! error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
91+
!!! error TS2339: Property 'a' does not exist on type 'BWithMethod'.
92+
x.b();
93+
~
94+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
95+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod'.
96+
}
97+
}
98+
99+
class C { a: string }
100+
class D { a: string }
101+
102+
function negativeMultipleClassesTest(x: A | B | C | D) {
103+
if ("a" in x) {
104+
x.b = "1";
105+
~
106+
!!! error TS2339: Property 'b' does not exist on type 'A | C | D'.
107+
!!! error TS2339: Property 'b' does not exist on type 'A'.
108+
} else {
109+
x.a = "1";
110+
~
111+
!!! error TS2339: Property 'a' does not exist on type 'B'.
112+
}
113+
}
114+
115+
class ClassWithProp { prop: A | B }
116+
117+
function negativePropTest(x: ClassWithProp) {
118+
if ("a" in x.prop) {
119+
let y: string = x.prop.b;
120+
~
121+
!!! error TS2339: Property 'b' does not exist on type 'A'.
122+
} else {
123+
let z: string = x.prop.a;
124+
~
125+
!!! error TS2339: Property 'a' does not exist on type 'B'.
126+
}
127+
}
128+
129+
class NegativeClassTest {
130+
protected prop: A | B;
131+
inThis() {
132+
if ('a' in this.prop) {
133+
let z: number = this.prop.b;
134+
~
135+
!!! error TS2339: Property 'b' does not exist on type 'A'.
136+
} else {
137+
let y: string = this.prop.a;
138+
~
139+
!!! error TS2339: Property 'a' does not exist on type 'B'.
140+
}
141+
}
142+
}
143+
144+
class UnreachableCodeDetection {
145+
a: string;
146+
inThis() {
147+
if ('a' in this) {
148+
} else {
149+
let y = this.a;
150+
~
151+
!!! error TS2339: Property 'a' does not exist on type 'never'.
152+
}
153+
}
154+
}

0 commit comments

Comments
 (0)