Skip to content

Commit c2fc5ea

Browse files
Merge pull request #15256 from IdeaHunter/in-typeguard
Add type guard for `in` keyword
2 parents 06aeab8 + 04b9b30 commit c2fc5ea

14 files changed

+2000
-8
lines changed

src/compiler/binder.ts

+6
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

+22
Original file line numberDiff line numberDiff line change
@@ -12722,6 +12722,22 @@ namespace ts {
1272212722
return type;
1272312723
}
1272412724

12725+
function isTypePresencePossible(type: Type, propName: __String, assumeTrue: boolean) {
12726+
const prop = getPropertyOfType(type, propName);
12727+
if (prop) {
12728+
return (prop.flags & SymbolFlags.Optional) ? true : assumeTrue;
12729+
}
12730+
return !assumeTrue;
12731+
}
12732+
12733+
function narrowByInKeyword(type: Type, literal: LiteralExpression, assumeTrue: boolean) {
12734+
if ((type.flags & (TypeFlags.Union | TypeFlags.Object)) || (type.flags & TypeFlags.TypeParameter && (type as TypeParameter).isThisType)) {
12735+
const propName = escapeLeadingUnderscores(literal.text);
12736+
return filterType(type, t => isTypePresencePossible(t, propName, /* assumeTrue */ assumeTrue));
12737+
}
12738+
return type;
12739+
}
12740+
1272512741
function narrowTypeByBinaryExpression(type: Type, expr: BinaryExpression, assumeTrue: boolean): Type {
1272612742
switch (expr.operatorToken.kind) {
1272712743
case SyntaxKind.EqualsToken:
@@ -12757,6 +12773,12 @@ namespace ts {
1275712773
break;
1275812774
case SyntaxKind.InstanceOfKeyword:
1275912775
return narrowTypeByInstanceof(type, expr, assumeTrue);
12776+
case SyntaxKind.InKeyword:
12777+
const target = getReferenceCandidate(expr.right);
12778+
if (expr.left.kind === SyntaxKind.StringLiteral && isMatchingReference(reference, target)) {
12779+
return narrowByInKeyword(type, <LiteralExpression>expr.left, assumeTrue);
12780+
}
12781+
break;
1276012782
case SyntaxKind.CommaToken:
1276112783
return narrowType(type, expr.right, assumeTrue);
1276212784
}

tests/baselines/reference/fixSignatureCaching.errors.txt

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ tests/cases/conformance/fixSignatureCaching.ts(284,10): error TS2339: Property '
33
tests/cases/conformance/fixSignatureCaching.ts(293,10): error TS2339: Property 'FALLBACK_PHONE' does not exist on type '{}'.
44
tests/cases/conformance/fixSignatureCaching.ts(294,10): error TS2339: Property 'FALLBACK_TABLET' does not exist on type '{}'.
55
tests/cases/conformance/fixSignatureCaching.ts(295,10): error TS2339: Property 'FALLBACK_MOBILE' does not exist on type '{}'.
6+
tests/cases/conformance/fixSignatureCaching.ts(301,17): error TS2339: Property 'isArray' does not exist on type 'never'.
67
tests/cases/conformance/fixSignatureCaching.ts(330,74): error TS2339: Property 'mobileDetectRules' does not exist on type '{}'.
78
tests/cases/conformance/fixSignatureCaching.ts(369,10): error TS2339: Property 'findMatch' does not exist on type '{}'.
89
tests/cases/conformance/fixSignatureCaching.ts(387,10): error TS2339: Property 'findMatches' does not exist on type '{}'.
@@ -71,7 +72,7 @@ tests/cases/conformance/fixSignatureCaching.ts(982,23): error TS2304: Cannot fin
7172
tests/cases/conformance/fixSignatureCaching.ts(983,37): error TS2304: Cannot find name 'window'.
7273

7374

74-
==== tests/cases/conformance/fixSignatureCaching.ts (71 errors) ====
75+
==== tests/cases/conformance/fixSignatureCaching.ts (72 errors) ====
7576
// Repro from #10697
7677

7778
(function (define, undefined) {
@@ -383,6 +384,8 @@ tests/cases/conformance/fixSignatureCaching.ts(983,37): error TS2304: Cannot fin
383384
isArray = 'isArray' in Array
384385
? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; }
385386
: Array.isArray;
387+
~~~~~~~
388+
!!! error TS2339: Property 'isArray' does not exist on type 'never'.
386389

387390
function equalIC(a, b) {
388391
return a != null && b != null && a.toLowerCase() === b.toLowerCase();

tests/baselines/reference/fixSignatureCaching.symbols

-2
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,7 @@ define(function () {
358358
>value : Symbol(value, Decl(fixSignatureCaching.ts, 299, 20))
359359

360360
: Array.isArray;
361-
>Array.isArray : Symbol(ArrayConstructor.isArray, Decl(lib.d.ts, --, --))
362361
>Array : Symbol(Array, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
363-
>isArray : Symbol(ArrayConstructor.isArray, Decl(lib.d.ts, --, --))
364362

365363
function equalIC(a, b) {
366364
>equalIC : Symbol(equalIC, Decl(fixSignatureCaching.ts, 300, 24))

tests/baselines/reference/fixSignatureCaching.types

+5-5
Original file line numberDiff line numberDiff line change
@@ -893,9 +893,9 @@ define(function () {
893893
>'[object Array]' : "[object Array]"
894894

895895
isArray = 'isArray' in Array
896-
>isArray = 'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : (value: any) => boolean
896+
>isArray = 'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : any
897897
>isArray : any
898-
>'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : (value: any) => boolean
898+
>'isArray' in Array ? function (value) { return Object.prototype.toString.call(value) === '[object Array]'; } : Array.isArray : any
899899
>'isArray' in Array : boolean
900900
>'isArray' : "isArray"
901901
>Array : ArrayConstructor
@@ -916,9 +916,9 @@ define(function () {
916916
>'[object Array]' : "[object Array]"
917917

918918
: Array.isArray;
919-
>Array.isArray : (arg: any) => arg is any[]
920-
>Array : ArrayConstructor
921-
>isArray : (arg: any) => arg is any[]
919+
>Array.isArray : any
920+
>Array : never
921+
>isArray : any
922922

923923
function equalIC(a, b) {
924924
>equalIC : (a: any, b: any) => boolean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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(27,11): error TS2339: Property 'b' does not exist on type 'AWithOptionalProp | BWithOptionalProp'.
6+
Property 'b' does not exist on type 'AWithOptionalProp'.
7+
tests/cases/compiler/inKeywordTypeguard.ts(42,11): error TS2339: Property 'b' does not exist on type 'AWithMethod'.
8+
tests/cases/compiler/inKeywordTypeguard.ts(49,11): error TS2339: Property 'a' does not exist on type 'never'.
9+
tests/cases/compiler/inKeywordTypeguard.ts(50,11): error TS2339: Property 'b' does not exist on type 'never'.
10+
tests/cases/compiler/inKeywordTypeguard.ts(52,11): error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
11+
Property 'a' does not exist on type 'BWithMethod'.
12+
tests/cases/compiler/inKeywordTypeguard.ts(53,11): error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
13+
Property 'b' does not exist on type 'AWithMethod'.
14+
tests/cases/compiler/inKeywordTypeguard.ts(62,11): error TS2339: Property 'b' does not exist on type 'A | C | D'.
15+
Property 'b' does not exist on type 'A'.
16+
tests/cases/compiler/inKeywordTypeguard.ts(64,11): error TS2339: Property 'a' does not exist on type 'B'.
17+
tests/cases/compiler/inKeywordTypeguard.ts(72,32): error TS2339: Property 'b' does not exist on type 'A'.
18+
tests/cases/compiler/inKeywordTypeguard.ts(74,32): error TS2339: Property 'a' does not exist on type 'B'.
19+
tests/cases/compiler/inKeywordTypeguard.ts(82,39): error TS2339: Property 'b' does not exist on type 'A'.
20+
tests/cases/compiler/inKeywordTypeguard.ts(84,39): error TS2339: Property 'a' does not exist on type 'B'.
21+
tests/cases/compiler/inKeywordTypeguard.ts(94,26): error TS2339: Property 'a' does not exist on type 'never'.
22+
23+
24+
==== tests/cases/compiler/inKeywordTypeguard.ts (17 errors) ====
25+
class A { a: string; }
26+
class B { b: string; }
27+
28+
function negativeClassesTest(x: A | B) {
29+
if ("a" in x) {
30+
x.b = "1";
31+
~
32+
!!! error TS2339: Property 'b' does not exist on type 'A'.
33+
} else {
34+
x.a = "1";
35+
~
36+
!!! error TS2339: Property 'a' does not exist on type 'B'.
37+
}
38+
}
39+
40+
function positiveClassesTest(x: A | B) {
41+
if ("a" in x) {
42+
x.b = "1";
43+
~
44+
!!! error TS2339: Property 'b' does not exist on type 'A'.
45+
} else {
46+
x.a = "1";
47+
~
48+
!!! error TS2339: Property 'a' does not exist on type 'B'.
49+
}
50+
}
51+
52+
class AWithOptionalProp { a?: string; }
53+
class BWithOptionalProp { b?: string; }
54+
55+
function positiveTestClassesWithOptionalProperties(x: AWithOptionalProp | BWithOptionalProp) {
56+
if ("a" in x) {
57+
x.a = "1";
58+
} else {
59+
x.b = "1";
60+
~
61+
!!! error TS2339: Property 'b' does not exist on type 'AWithOptionalProp | BWithOptionalProp'.
62+
!!! error TS2339: Property 'b' does not exist on type 'AWithOptionalProp'.
63+
}
64+
}
65+
66+
class AWithMethod {
67+
a(): string { return ""; }
68+
}
69+
70+
class BWithMethod {
71+
b(): string { return ""; }
72+
}
73+
74+
function negativeTestClassesWithMembers(x: AWithMethod | BWithMethod) {
75+
if ("a" in x) {
76+
x.a();
77+
x.b();
78+
~
79+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod'.
80+
} else {
81+
}
82+
}
83+
84+
function negativeTestClassesWithMemberMissingInBothClasses(x: AWithMethod | BWithMethod) {
85+
if ("c" in x) {
86+
x.a();
87+
~
88+
!!! error TS2339: Property 'a' does not exist on type 'never'.
89+
x.b();
90+
~
91+
!!! error TS2339: Property 'b' does not exist on type 'never'.
92+
} else {
93+
x.a();
94+
~
95+
!!! error TS2339: Property 'a' does not exist on type 'AWithMethod | BWithMethod'.
96+
!!! error TS2339: Property 'a' does not exist on type 'BWithMethod'.
97+
x.b();
98+
~
99+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod | BWithMethod'.
100+
!!! error TS2339: Property 'b' does not exist on type 'AWithMethod'.
101+
}
102+
}
103+
104+
class C { a: string; }
105+
class D { a: string; }
106+
107+
function negativeMultipleClassesTest(x: A | B | C | D) {
108+
if ("a" in x) {
109+
x.b = "1";
110+
~
111+
!!! error TS2339: Property 'b' does not exist on type 'A | C | D'.
112+
!!! error TS2339: Property 'b' does not exist on type 'A'.
113+
} else {
114+
x.a = "1";
115+
~
116+
!!! error TS2339: Property 'a' does not exist on type 'B'.
117+
}
118+
}
119+
120+
class ClassWithUnionProp { prop: A | B }
121+
122+
function negativePropTest(x: ClassWithUnionProp) {
123+
if ("a" in x.prop) {
124+
let y: string = x.prop.b;
125+
~
126+
!!! error TS2339: Property 'b' does not exist on type 'A'.
127+
} else {
128+
let z: string = x.prop.a;
129+
~
130+
!!! error TS2339: Property 'a' does not exist on type 'B'.
131+
}
132+
}
133+
134+
class NegativeClassTest {
135+
protected prop: A | B;
136+
inThis() {
137+
if ("a" in this.prop) {
138+
let z: number = this.prop.b;
139+
~
140+
!!! error TS2339: Property 'b' does not exist on type 'A'.
141+
} else {
142+
let y: string = this.prop.a;
143+
~
144+
!!! error TS2339: Property 'a' does not exist on type 'B'.
145+
}
146+
}
147+
}
148+
149+
class UnreachableCodeDetection {
150+
a: string;
151+
inThis() {
152+
if ("a" in this) {
153+
} else {
154+
let y = this.a;
155+
~
156+
!!! error TS2339: Property 'a' does not exist on type 'never'.
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)