Skip to content

Commit 9289cfb

Browse files
committed
Merge pull request #1803 from Microsoft/enumUnionTypeGuard
Fixes to union types in type guards and instanceof
2 parents ba5a612 + 35b2038 commit 9289cfb

File tree

8 files changed

+274
-17
lines changed

8 files changed

+274
-17
lines changed

src/compiler/checker.ts

+46-15
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ module ts {
107107
var diagnostics: Diagnostic[] = [];
108108
var diagnosticsModified: boolean = false;
109109

110+
var primitiveTypeInfo: Map<{ type: Type; flags: TypeFlags }> = {
111+
"string": {
112+
type: stringType,
113+
flags: TypeFlags.StringLike
114+
},
115+
"number": {
116+
type: numberType,
117+
flags: TypeFlags.NumberLike
118+
},
119+
"boolean": {
120+
type: booleanType,
121+
flags: TypeFlags.Boolean
122+
}
123+
};
124+
110125
function addDiagnostic(diagnostic: Diagnostic) {
111126
diagnostics.push(diagnostic);
112127
diagnosticsModified = true;
@@ -4482,12 +4497,17 @@ module ts {
44824497
Debug.fail("should not get here");
44834498
}
44844499

4485-
// Remove one or more primitive types from a union type
4486-
function subtractPrimitiveTypes(type: Type, subtractMask: TypeFlags): Type {
4500+
// For a union type, remove all constituent types that are of the given type kind (when isOfTypeKind is true)
4501+
// or not of the given type kind (when isOfTypeKind is false)
4502+
function removeTypesFromUnionType(type: Type, typeKind: TypeFlags, isOfTypeKind: boolean): Type {
44874503
if (type.flags & TypeFlags.Union) {
44884504
var types = (<UnionType>type).types;
4489-
if (forEach(types, t => t.flags & subtractMask)) {
4490-
return getUnionType(filter(types, t => !(t.flags & subtractMask)));
4505+
if (forEach(types, t => !!(t.flags & typeKind) === isOfTypeKind)) {
4506+
// Above we checked if we have anything to remove, now use the opposite test to do the removal
4507+
var narrowedType = getUnionType(filter(types, t => !(t.flags & typeKind) === isOfTypeKind));
4508+
if (narrowedType !== emptyObjectType) {
4509+
return narrowedType;
4510+
}
44914511
}
44924512
}
44934513
return type;
@@ -4663,8 +4683,8 @@ module ts {
46634683
// Stop at the first containing function or module declaration
46644684
break loop;
46654685
}
4666-
// Use narrowed type if it is a subtype and construct contains no assignments to variable
4667-
if (narrowedType !== type && isTypeSubtypeOf(narrowedType, type)) {
4686+
// Use narrowed type if construct contains no assignments to variable
4687+
if (narrowedType !== type) {
46684688
if (isVariableAssignedWithin(symbol, node)) {
46694689
break;
46704690
}
@@ -4684,20 +4704,30 @@ module ts {
46844704
if (left.expression.kind !== SyntaxKind.Identifier || getResolvedSymbol(<Identifier>left.expression) !== symbol) {
46854705
return type;
46864706
}
4687-
var t = right.text;
4688-
var checkType: Type = t === "string" ? stringType : t === "number" ? numberType : t === "boolean" ? booleanType : emptyObjectType;
4707+
var typeInfo = primitiveTypeInfo[right.text];
46894708
if (expr.operator === SyntaxKind.ExclamationEqualsEqualsToken) {
46904709
assumeTrue = !assumeTrue;
46914710
}
46924711
if (assumeTrue) {
4693-
// The assumed result is true. If check was for a primitive type, that type is the narrowed type. Otherwise we can
4694-
// remove the primitive types from the narrowed type.
4695-
return checkType === emptyObjectType ? subtractPrimitiveTypes(type, TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean) : checkType;
4712+
// Assumed result is true. If check was not for a primitive type, remove all primitive types
4713+
if (!typeInfo) {
4714+
return removeTypesFromUnionType(type, /*typeKind*/ TypeFlags.StringLike | TypeFlags.NumberLike | TypeFlags.Boolean, /*isOfTypeKind*/ true);
4715+
}
4716+
// Check was for a primitive type, return that primitive type if it is a subtype
4717+
if (isTypeSubtypeOf(typeInfo.type, type)) {
4718+
return typeInfo.type;
4719+
}
4720+
// Otherwise, remove all types that aren't of the primitive type kind. This can happen when the type is
4721+
// union of enum types and other types.
4722+
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ false);
46964723
}
46974724
else {
4698-
// The assumed result is false. If check was for a primitive type we can remove that type from the narrowed type.
4725+
// Assumed result is false. If check was for a primitive type, remove that primitive type
4726+
if (typeInfo) {
4727+
return removeTypesFromUnionType(type, /*typeKind*/ typeInfo.flags, /*isOfTypeKind*/ true);
4728+
}
46994729
// Otherwise we don't have enough information to do anything.
4700-
return checkType === emptyObjectType ? type : subtractPrimitiveTypes(type, checkType.flags);
4730+
return type;
47014731
}
47024732
}
47034733

@@ -4758,7 +4788,8 @@ module ts {
47584788
return type;
47594789
}
47604790

4761-
// Narrow the given type based on the given expression having the assumed boolean value
4791+
// Narrow the given type based on the given expression having the assumed boolean value. The returned type
4792+
// will be a subtype or the same type as the argument.
47624793
function narrowType(type: Type, expr: Expression, assumeTrue: boolean): Type {
47634794
switch (expr.kind) {
47644795
case SyntaxKind.ParenthesizedExpression:
@@ -6789,7 +6820,7 @@ module ts {
67896820
// and the right operand to be of type Any or a subtype of the 'Function' interface type.
67906821
// The result is always of the Boolean primitive type.
67916822
// NOTE: do not raise error if leftType is unknown as related error was already reported
6792-
if (!isTypeOfKind(leftType, TypeFlags.Any | TypeFlags.ObjectType | TypeFlags.TypeParameter)) {
6823+
if (isTypeOfKind(leftType, TypeFlags.Primitive)) {
67936824
error(node.left, Diagnostics.The_left_hand_side_of_an_instanceof_expression_must_be_of_type_any_an_object_type_or_a_type_parameter);
67946825
}
67956826
// NOTE: do not raise error if right is unknown as related error was already reported

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,7 @@ module ts {
12881288
ContainsObjectLiteral = 0x00080000, // Type is or contains object literal type
12891289

12901290
Intrinsic = Any | String | Number | Boolean | Void | Undefined | Null,
1291+
Primitive = String | Number | Boolean | Void | Undefined | Null | StringLiteral | Enum,
12911292
StringLike = String | StringLiteral,
12921293
NumberLike = Number | Enum,
12931294
ObjectType = Class | Interface | Reference | Tuple | Anonymous,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//// [TypeGuardWithEnumUnion.ts]
2+
enum Color { R, G, B }
3+
4+
function f1(x: Color | string) {
5+
if (typeof x === "number") {
6+
var y = x;
7+
var y: Color;
8+
}
9+
else {
10+
var z = x;
11+
var z: string;
12+
}
13+
}
14+
15+
function f2(x: Color | string | string[]) {
16+
if (typeof x === "object") {
17+
var y = x;
18+
var y: string[];
19+
}
20+
if (typeof x === "number") {
21+
var z = x;
22+
var z: Color;
23+
}
24+
else {
25+
var w = x;
26+
var w: string | string[];
27+
}
28+
if (typeof x === "string") {
29+
var a = x;
30+
var a: string;
31+
}
32+
else {
33+
var b = x;
34+
var b: Color | string[];
35+
}
36+
}
37+
38+
39+
//// [TypeGuardWithEnumUnion.js]
40+
var Color;
41+
(function (Color) {
42+
Color[Color["R"] = 0] = "R";
43+
Color[Color["G"] = 1] = "G";
44+
Color[Color["B"] = 2] = "B";
45+
})(Color || (Color = {}));
46+
function f1(x) {
47+
if (typeof x === "number") {
48+
var y = x;
49+
var y;
50+
}
51+
else {
52+
var z = x;
53+
var z;
54+
}
55+
}
56+
function f2(x) {
57+
if (typeof x === "object") {
58+
var y = x;
59+
var y;
60+
}
61+
if (typeof x === "number") {
62+
var z = x;
63+
var z;
64+
}
65+
else {
66+
var w = x;
67+
var w;
68+
}
69+
if (typeof x === "string") {
70+
var a = x;
71+
var a;
72+
}
73+
else {
74+
var b = x;
75+
var b;
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
=== tests/cases/conformance/expressions/typeGuards/TypeGuardWithEnumUnion.ts ===
2+
enum Color { R, G, B }
3+
>Color : Color
4+
>R : Color
5+
>G : Color
6+
>B : Color
7+
8+
function f1(x: Color | string) {
9+
>f1 : (x: string | Color) => void
10+
>x : string | Color
11+
>Color : Color
12+
13+
if (typeof x === "number") {
14+
>typeof x === "number" : boolean
15+
>typeof x : string
16+
>x : string | Color
17+
18+
var y = x;
19+
>y : Color
20+
>x : Color
21+
22+
var y: Color;
23+
>y : Color
24+
>Color : Color
25+
}
26+
else {
27+
var z = x;
28+
>z : string
29+
>x : string
30+
31+
var z: string;
32+
>z : string
33+
}
34+
}
35+
36+
function f2(x: Color | string | string[]) {
37+
>f2 : (x: string | string[] | Color) => void
38+
>x : string | string[] | Color
39+
>Color : Color
40+
41+
if (typeof x === "object") {
42+
>typeof x === "object" : boolean
43+
>typeof x : string
44+
>x : string | string[] | Color
45+
46+
var y = x;
47+
>y : string[]
48+
>x : string[]
49+
50+
var y: string[];
51+
>y : string[]
52+
}
53+
if (typeof x === "number") {
54+
>typeof x === "number" : boolean
55+
>typeof x : string
56+
>x : string | string[] | Color
57+
58+
var z = x;
59+
>z : Color
60+
>x : Color
61+
62+
var z: Color;
63+
>z : Color
64+
>Color : Color
65+
}
66+
else {
67+
var w = x;
68+
>w : string | string[]
69+
>x : string | string[]
70+
71+
var w: string | string[];
72+
>w : string | string[]
73+
}
74+
if (typeof x === "string") {
75+
>typeof x === "string" : boolean
76+
>typeof x : string
77+
>x : string | string[] | Color
78+
79+
var a = x;
80+
>a : string
81+
>x : string
82+
83+
var a: string;
84+
>a : string
85+
}
86+
else {
87+
var b = x;
88+
>b : string[] | Color
89+
>x : string[] | Color
90+
91+
var b: Color | string[];
92+
>b : string[] | Color
93+
>Color : Color
94+
}
95+
}
96+

tests/baselines/reference/instanceofOperatorWithLHSIsObject.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ var x2: Function;
77
var a: {};
88
var b: Object;
99
var c: C;
10+
var d: string | C;
1011

1112
var r1 = a instanceof x1;
1213
var r2 = b instanceof x2;
13-
var r3 = c instanceof x1;
14+
var r3 = c instanceof x1;
15+
var r4 = d instanceof x1;
16+
1417

1518
//// [instanceofOperatorWithLHSIsObject.js]
1619
var C = (function () {
@@ -23,6 +26,8 @@ var x2;
2326
var a;
2427
var b;
2528
var c;
29+
var d;
2630
var r1 = a instanceof x1;
2731
var r2 = b instanceof x2;
2832
var r3 = c instanceof x1;
33+
var r4 = d instanceof x1;

tests/baselines/reference/instanceofOperatorWithLHSIsObject.types

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ var c: C;
2020
>c : C
2121
>C : C
2222

23+
var d: string | C;
24+
>d : string | C
25+
>C : C
26+
2327
var r1 = a instanceof x1;
2428
>r1 : boolean
2529
>a instanceof x1 : boolean
@@ -38,3 +42,9 @@ var r3 = c instanceof x1;
3842
>c : C
3943
>x1 : any
4044

45+
var r4 = d instanceof x1;
46+
>r4 : boolean
47+
>d instanceof x1 : boolean
48+
>d : string | C
49+
>x1 : any
50+

tests/cases/conformance/expressions/binaryOperators/instanceofOperator/instanceofOperatorWithLHSIsObject.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ var x2: Function;
66
var a: {};
77
var b: Object;
88
var c: C;
9+
var d: string | C;
910

1011
var r1 = a instanceof x1;
1112
var r2 = b instanceof x2;
12-
var r3 = c instanceof x1;
13+
var r3 = c instanceof x1;
14+
var r4 = d instanceof x1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
enum Color { R, G, B }
2+
3+
function f1(x: Color | string) {
4+
if (typeof x === "number") {
5+
var y = x;
6+
var y: Color;
7+
}
8+
else {
9+
var z = x;
10+
var z: string;
11+
}
12+
}
13+
14+
function f2(x: Color | string | string[]) {
15+
if (typeof x === "object") {
16+
var y = x;
17+
var y: string[];
18+
}
19+
if (typeof x === "number") {
20+
var z = x;
21+
var z: Color;
22+
}
23+
else {
24+
var w = x;
25+
var w: string | string[];
26+
}
27+
if (typeof x === "string") {
28+
var a = x;
29+
var a: string;
30+
}
31+
else {
32+
var b = x;
33+
var b: Color | string[];
34+
}
35+
}

0 commit comments

Comments
 (0)