Skip to content

Commit 17c2ab2

Browse files
authored
Merge pull request #11587 from Microsoft/narrowStringAndNumber
Narrow string and number types in literal equality checks
2 parents 31a55e6 + 8bcb22c commit 17c2ab2

14 files changed

+551
-22
lines changed

src/compiler/checker.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -8461,6 +8461,28 @@ namespace ts {
84618461
return f(type) ? type : neverType;
84628462
}
84638463

8464+
function mapType(type: Type, f: (t: Type) => Type): Type {
8465+
return type.flags & TypeFlags.Union ? getUnionType(map((<UnionType>type).types, f)) : f(type);
8466+
}
8467+
8468+
function extractTypesOfKind(type: Type, kind: TypeFlags) {
8469+
return filterType(type, t => (t.flags & kind) !== 0);
8470+
}
8471+
8472+
// Return a new type in which occurrences of the string and number primitive types in
8473+
// typeWithPrimitives have been replaced with occurrences of string literals and numeric
8474+
// literals in typeWithLiterals, respectively.
8475+
function replacePrimitivesWithLiterals(typeWithPrimitives: Type, typeWithLiterals: Type) {
8476+
if (isTypeSubsetOf(stringType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.StringLiteral) ||
8477+
isTypeSubsetOf(numberType, typeWithPrimitives) && maybeTypeOfKind(typeWithLiterals, TypeFlags.NumberLiteral)) {
8478+
return mapType(typeWithPrimitives, t =>
8479+
t.flags & TypeFlags.String ? extractTypesOfKind(typeWithLiterals, TypeFlags.String | TypeFlags.StringLiteral) :
8480+
t.flags & TypeFlags.Number ? extractTypesOfKind(typeWithLiterals, TypeFlags.Number | TypeFlags.NumberLiteral) :
8481+
t);
8482+
}
8483+
return typeWithPrimitives;
8484+
}
8485+
84648486
function isIncomplete(flowType: FlowType) {
84658487
return flowType.flags === 0;
84668488
}
@@ -8796,7 +8818,7 @@ namespace ts {
87968818
}
87978819
if (assumeTrue) {
87988820
const narrowedType = filterType(type, t => areTypesComparable(t, valueType));
8799-
return narrowedType.flags & TypeFlags.Never ? type : narrowedType;
8821+
return narrowedType.flags & TypeFlags.Never ? type : replacePrimitivesWithLiterals(narrowedType, valueType);
88008822
}
88018823
if (isUnitType(valueType)) {
88028824
const regularType = getRegularTypeOfLiteralType(valueType);
@@ -8843,7 +8865,9 @@ namespace ts {
88438865
const clauseTypes = switchTypes.slice(clauseStart, clauseEnd);
88448866
const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, neverType);
88458867
const discriminantType = getUnionType(clauseTypes);
8846-
const caseType = discriminantType.flags & TypeFlags.Never ? neverType : filterType(type, t => isTypeComparableTo(discriminantType, t));
8868+
const caseType =
8869+
discriminantType.flags & TypeFlags.Never ? neverType :
8870+
replacePrimitivesWithLiterals(filterType(type, t => isTypeComparableTo(discriminantType, t)), discriminantType);
88478871
if (!hasDefaultClause) {
88488872
return caseType;
88498873
}

src/compiler/scanner.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1190,7 +1190,7 @@ namespace ts {
11901190
}
11911191

11921192
function scanBinaryOrOctalDigits(base: number): number {
1193-
Debug.assert(base !== 2 || base !== 8, "Expected either base 2 or base 8");
1193+
Debug.assert(base === 2 || base === 8, "Expected either base 2 or base 8");
11941194

11951195
let value = 0;
11961196
// For counting number of digits; Valid binaryIntegerLiteral must have at least one binary digit following B or b.

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2644,7 +2644,7 @@ namespace ts {
26442644
// 'Narrowable' types are types where narrowing actually narrows.
26452645
// This *should* be every type other than null, undefined, void, and never
26462646
Narrowable = Any | StructuredType | TypeParameter | StringLike | NumberLike | BooleanLike | ESSymbol,
2647-
NotUnionOrUnit = Any | String | Number | ESSymbol | ObjectType,
2647+
NotUnionOrUnit = Any | ESSymbol | ObjectType,
26482648
/* @internal */
26492649
RequiresWidening = ContainsWideningType | ContainsObjectLiteral,
26502650
/* @internal */

tests/baselines/reference/declFileTypeAnnotationStringLiteral.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function foo(a: string): string | number {
2323

2424
return a.length;
2525
>a.length : number
26-
>a : string
26+
>a : "hello"
2727
>length : number
2828
}
2929

tests/baselines/reference/literalTypes1.types

+4-4
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function f4(x: 0 | 1 | true | string) {
129129
>"def" : "def"
130130

131131
x;
132-
>x : string
132+
>x : "abc" | "def"
133133

134134
break;
135135
case null:
@@ -163,7 +163,7 @@ function f5(x: string | number | boolean) {
163163
>"abc" : "abc"
164164

165165
x;
166-
>x : string
166+
>x : "abc"
167167

168168
break;
169169
case 0:
@@ -173,7 +173,7 @@ function f5(x: string | number | boolean) {
173173
>1 : 1
174174

175175
x;
176-
>x : number
176+
>x : 0 | 1
177177

178178
break;
179179
case true:
@@ -190,7 +190,7 @@ function f5(x: string | number | boolean) {
190190
>123 : 123
191191

192192
x;
193-
>x : string | number
193+
>x : "hello" | 123
194194

195195
break;
196196
default:
+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//// [literalTypes3.ts]
2+
3+
function f1(s: string) {
4+
if (s === "foo") {
5+
s; // "foo"
6+
}
7+
if (s === "foo" || s === "bar") {
8+
s; // "foo" | "bar"
9+
}
10+
}
11+
12+
function f2(s: string) {
13+
switch (s) {
14+
case "foo":
15+
case "bar":
16+
s; // "foo" | "bar"
17+
case "baz":
18+
s; // "foo" | "bar" | "baz"
19+
break;
20+
default:
21+
s; // string
22+
}
23+
}
24+
25+
function f3(s: string) {
26+
return s === "foo" || s === "bar" ? s : undefined; // "foo" | "bar" | undefined
27+
}
28+
29+
function f4(x: number) {
30+
if (x === 1 || x === 2) {
31+
return x; // 1 | 2
32+
}
33+
throw new Error();
34+
}
35+
36+
function f5(x: number, y: 1 | 2) {
37+
if (x === 0 || x === y) {
38+
x; // 0 | 1 | 2
39+
}
40+
}
41+
42+
function f6(x: number, y: 1 | 2) {
43+
if (y === x || 0 === x) {
44+
x; // 0 | 1 | 2
45+
}
46+
}
47+
48+
function f7(x: number | "foo" | "bar", y: 1 | 2 | string) {
49+
if (x === y) {
50+
x; // "foo" | "bar" | 1 | 2
51+
}
52+
}
53+
54+
function f8(x: number | "foo" | "bar") {
55+
switch (x) {
56+
case 1:
57+
case 2:
58+
x; // 1 | 2
59+
break;
60+
case "foo":
61+
x; // "foo"
62+
break;
63+
default:
64+
x; // number | "bar"
65+
}
66+
}
67+
68+
//// [literalTypes3.js]
69+
function f1(s) {
70+
if (s === "foo") {
71+
s; // "foo"
72+
}
73+
if (s === "foo" || s === "bar") {
74+
s; // "foo" | "bar"
75+
}
76+
}
77+
function f2(s) {
78+
switch (s) {
79+
case "foo":
80+
case "bar":
81+
s; // "foo" | "bar"
82+
case "baz":
83+
s; // "foo" | "bar" | "baz"
84+
break;
85+
default:
86+
s; // string
87+
}
88+
}
89+
function f3(s) {
90+
return s === "foo" || s === "bar" ? s : undefined; // "foo" | "bar" | undefined
91+
}
92+
function f4(x) {
93+
if (x === 1 || x === 2) {
94+
return x; // 1 | 2
95+
}
96+
throw new Error();
97+
}
98+
function f5(x, y) {
99+
if (x === 0 || x === y) {
100+
x; // 0 | 1 | 2
101+
}
102+
}
103+
function f6(x, y) {
104+
if (y === x || 0 === x) {
105+
x; // 0 | 1 | 2
106+
}
107+
}
108+
function f7(x, y) {
109+
if (x === y) {
110+
x; // "foo" | "bar" | 1 | 2
111+
}
112+
}
113+
function f8(x) {
114+
switch (x) {
115+
case 1:
116+
case 2:
117+
x; // 1 | 2
118+
break;
119+
case "foo":
120+
x; // "foo"
121+
break;
122+
default:
123+
x; // number | "bar"
124+
}
125+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
=== tests/cases/conformance/types/literal/literalTypes3.ts ===
2+
3+
function f1(s: string) {
4+
>f1 : Symbol(f1, Decl(literalTypes3.ts, 0, 0))
5+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
6+
7+
if (s === "foo") {
8+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
9+
10+
s; // "foo"
11+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
12+
}
13+
if (s === "foo" || s === "bar") {
14+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
15+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
16+
17+
s; // "foo" | "bar"
18+
>s : Symbol(s, Decl(literalTypes3.ts, 1, 12))
19+
}
20+
}
21+
22+
function f2(s: string) {
23+
>f2 : Symbol(f2, Decl(literalTypes3.ts, 8, 1))
24+
>s : Symbol(s, Decl(literalTypes3.ts, 10, 12))
25+
26+
switch (s) {
27+
>s : Symbol(s, Decl(literalTypes3.ts, 10, 12))
28+
29+
case "foo":
30+
case "bar":
31+
s; // "foo" | "bar"
32+
>s : Symbol(s, Decl(literalTypes3.ts, 10, 12))
33+
34+
case "baz":
35+
s; // "foo" | "bar" | "baz"
36+
>s : Symbol(s, Decl(literalTypes3.ts, 10, 12))
37+
38+
break;
39+
default:
40+
s; // string
41+
>s : Symbol(s, Decl(literalTypes3.ts, 10, 12))
42+
}
43+
}
44+
45+
function f3(s: string) {
46+
>f3 : Symbol(f3, Decl(literalTypes3.ts, 21, 1))
47+
>s : Symbol(s, Decl(literalTypes3.ts, 23, 12))
48+
49+
return s === "foo" || s === "bar" ? s : undefined; // "foo" | "bar" | undefined
50+
>s : Symbol(s, Decl(literalTypes3.ts, 23, 12))
51+
>s : Symbol(s, Decl(literalTypes3.ts, 23, 12))
52+
>s : Symbol(s, Decl(literalTypes3.ts, 23, 12))
53+
>undefined : Symbol(undefined)
54+
}
55+
56+
function f4(x: number) {
57+
>f4 : Symbol(f4, Decl(literalTypes3.ts, 25, 1))
58+
>x : Symbol(x, Decl(literalTypes3.ts, 27, 12))
59+
60+
if (x === 1 || x === 2) {
61+
>x : Symbol(x, Decl(literalTypes3.ts, 27, 12))
62+
>x : Symbol(x, Decl(literalTypes3.ts, 27, 12))
63+
64+
return x; // 1 | 2
65+
>x : Symbol(x, Decl(literalTypes3.ts, 27, 12))
66+
}
67+
throw new Error();
68+
>Error : Symbol(Error, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
69+
}
70+
71+
function f5(x: number, y: 1 | 2) {
72+
>f5 : Symbol(f5, Decl(literalTypes3.ts, 32, 1))
73+
>x : Symbol(x, Decl(literalTypes3.ts, 34, 12))
74+
>y : Symbol(y, Decl(literalTypes3.ts, 34, 22))
75+
76+
if (x === 0 || x === y) {
77+
>x : Symbol(x, Decl(literalTypes3.ts, 34, 12))
78+
>x : Symbol(x, Decl(literalTypes3.ts, 34, 12))
79+
>y : Symbol(y, Decl(literalTypes3.ts, 34, 22))
80+
81+
x; // 0 | 1 | 2
82+
>x : Symbol(x, Decl(literalTypes3.ts, 34, 12))
83+
}
84+
}
85+
86+
function f6(x: number, y: 1 | 2) {
87+
>f6 : Symbol(f6, Decl(literalTypes3.ts, 38, 1))
88+
>x : Symbol(x, Decl(literalTypes3.ts, 40, 12))
89+
>y : Symbol(y, Decl(literalTypes3.ts, 40, 22))
90+
91+
if (y === x || 0 === x) {
92+
>y : Symbol(y, Decl(literalTypes3.ts, 40, 22))
93+
>x : Symbol(x, Decl(literalTypes3.ts, 40, 12))
94+
>x : Symbol(x, Decl(literalTypes3.ts, 40, 12))
95+
96+
x; // 0 | 1 | 2
97+
>x : Symbol(x, Decl(literalTypes3.ts, 40, 12))
98+
}
99+
}
100+
101+
function f7(x: number | "foo" | "bar", y: 1 | 2 | string) {
102+
>f7 : Symbol(f7, Decl(literalTypes3.ts, 44, 1))
103+
>x : Symbol(x, Decl(literalTypes3.ts, 46, 12))
104+
>y : Symbol(y, Decl(literalTypes3.ts, 46, 38))
105+
106+
if (x === y) {
107+
>x : Symbol(x, Decl(literalTypes3.ts, 46, 12))
108+
>y : Symbol(y, Decl(literalTypes3.ts, 46, 38))
109+
110+
x; // "foo" | "bar" | 1 | 2
111+
>x : Symbol(x, Decl(literalTypes3.ts, 46, 12))
112+
}
113+
}
114+
115+
function f8(x: number | "foo" | "bar") {
116+
>f8 : Symbol(f8, Decl(literalTypes3.ts, 50, 1))
117+
>x : Symbol(x, Decl(literalTypes3.ts, 52, 12))
118+
119+
switch (x) {
120+
>x : Symbol(x, Decl(literalTypes3.ts, 52, 12))
121+
122+
case 1:
123+
case 2:
124+
x; // 1 | 2
125+
>x : Symbol(x, Decl(literalTypes3.ts, 52, 12))
126+
127+
break;
128+
case "foo":
129+
x; // "foo"
130+
>x : Symbol(x, Decl(literalTypes3.ts, 52, 12))
131+
132+
break;
133+
default:
134+
x; // number | "bar"
135+
>x : Symbol(x, Decl(literalTypes3.ts, 52, 12))
136+
}
137+
}

0 commit comments

Comments
 (0)