Skip to content

Commit 8c5ad24

Browse files
authored
Relax switch-case narrowing restrictions (#23522)
* Allow switch case narrowing even when individual clauses are nonunit * And remove unit type restriction * Rename
1 parent 47385b2 commit 8c5ad24

7 files changed

+280
-14
lines changed

src/compiler/checker.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -11570,6 +11570,10 @@ namespace ts {
1157011570
return !!getPropertyOfType(type, "0" as __String);
1157111571
}
1157211572

11573+
function isNeitherUnitTypeNorNever(type: Type): boolean {
11574+
return !(type.flags & (TypeFlags.Unit | TypeFlags.Never));
11575+
}
11576+
1157311577
function isUnitType(type: Type): boolean {
1157411578
return !!(type.flags & TypeFlags.Unit);
1157511579
}
@@ -13049,24 +13053,17 @@ namespace ts {
1304913053

1305013054
function getTypeOfSwitchClause(clause: CaseClause | DefaultClause) {
1305113055
if (clause.kind === SyntaxKind.CaseClause) {
13052-
const caseType = getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression));
13053-
return isUnitType(caseType) ? caseType : undefined;
13056+
return getRegularTypeOfLiteralType(getTypeOfExpression(clause.expression));
1305413057
}
1305513058
return neverType;
1305613059
}
1305713060

1305813061
function getSwitchClauseTypes(switchStatement: SwitchStatement): Type[] {
1305913062
const links = getNodeLinks(switchStatement);
1306013063
if (!links.switchTypes) {
13061-
// If all case clauses specify expressions that have unit types, we return an array
13062-
// of those unit types. Otherwise we return an empty array.
1306313064
links.switchTypes = [];
1306413065
for (const clause of switchStatement.caseBlock.clauses) {
13065-
const type = getTypeOfSwitchClause(clause);
13066-
if (type === undefined) {
13067-
return links.switchTypes = emptyArray;
13068-
}
13069-
links.switchTypes.push(type);
13066+
links.switchTypes.push(getTypeOfSwitchClause(clause));
1307013067
}
1307113068
}
1307213069
return links.switchTypes;
@@ -19170,7 +19167,7 @@ namespace ts {
1917019167
return false;
1917119168
}
1917219169
const switchTypes = getSwitchClauseTypes(node);
19173-
if (!switchTypes.length) {
19170+
if (!switchTypes.length || some(switchTypes, isNeitherUnitTypeNorNever)) {
1917419171
return false;
1917519172
}
1917619173
return eachTypeContainedIn(mapType(type, getRegularTypeOfLiteralType), switchTypes);

tests/baselines/reference/literalTypes1.types

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,19 @@ function f2(x: 0 | 1 | 2) {
6161
>zero : 0
6262

6363
x;
64-
>x : 0 | 1 | 2
64+
>x : 0
6565

6666
break;
6767
case oneOrTwo:
6868
>oneOrTwo : 1 | 2
6969

7070
x;
71-
>x : 0 | 1 | 2
71+
>x : 1 | 2
7272

7373
break;
7474
default:
7575
x;
76-
>x : 0 | 1 | 2
76+
>x : 1 | 2
7777
}
7878
}
7979

tests/baselines/reference/stringLiteralsWithSwitchStatements03.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ switch (x) {
5151
>"baz" : "baz"
5252

5353
x;
54-
>x : "foo"
54+
>x : never
5555

5656
y;
5757
>y : "foo" | "bar"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//// [switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts]
2+
export const narrowToLiterals = (str: string) => {
3+
switch (str) {
4+
case 'abc': {
5+
// inferred type as `abc`
6+
return str;
7+
}
8+
default:
9+
return 'defaultValue';
10+
}
11+
};
12+
13+
export const narrowToString = (str: string, someOtherStr: string) => {
14+
switch (str) {
15+
case 'abc': {
16+
// inferred type should be `abc`
17+
return str;
18+
}
19+
case someOtherStr: {
20+
// `string`
21+
return str;
22+
}
23+
default:
24+
return 'defaultValue';
25+
}
26+
};
27+
28+
export const narrowToStringOrNumber = (str: string | number, someNumber: number) => {
29+
switch (str) {
30+
case 'abc': {
31+
// inferred type should be `abc`
32+
return str;
33+
}
34+
case someNumber: {
35+
// inferred type should be `number`
36+
return str;
37+
}
38+
default:
39+
return 'defaultValue';
40+
}
41+
};
42+
43+
//// [switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.js]
44+
"use strict";
45+
exports.__esModule = true;
46+
exports.narrowToLiterals = function (str) {
47+
switch (str) {
48+
case 'abc': {
49+
// inferred type as `abc`
50+
return str;
51+
}
52+
default:
53+
return 'defaultValue';
54+
}
55+
};
56+
exports.narrowToString = function (str, someOtherStr) {
57+
switch (str) {
58+
case 'abc': {
59+
// inferred type should be `abc`
60+
return str;
61+
}
62+
case someOtherStr: {
63+
// `string`
64+
return str;
65+
}
66+
default:
67+
return 'defaultValue';
68+
}
69+
};
70+
exports.narrowToStringOrNumber = function (str, someNumber) {
71+
switch (str) {
72+
case 'abc': {
73+
// inferred type should be `abc`
74+
return str;
75+
}
76+
case someNumber: {
77+
// inferred type should be `number`
78+
return str;
79+
}
80+
default:
81+
return 'defaultValue';
82+
}
83+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
=== tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts ===
2+
export const narrowToLiterals = (str: string) => {
3+
>narrowToLiterals : Symbol(narrowToLiterals, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 12))
4+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33))
5+
6+
switch (str) {
7+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33))
8+
9+
case 'abc': {
10+
// inferred type as `abc`
11+
return str;
12+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 0, 33))
13+
}
14+
default:
15+
return 'defaultValue';
16+
}
17+
};
18+
19+
export const narrowToString = (str: string, someOtherStr: string) => {
20+
>narrowToString : Symbol(narrowToString, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 14))
21+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33))
22+
>someOtherStr : Symbol(someOtherStr, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 45))
23+
24+
switch (str) {
25+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33))
26+
27+
case 'abc': {
28+
// inferred type should be `abc`
29+
return str;
30+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33))
31+
}
32+
case someOtherStr: {
33+
>someOtherStr : Symbol(someOtherStr, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 45))
34+
35+
// `string`
36+
return str;
37+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 11, 33))
38+
}
39+
default:
40+
return 'defaultValue';
41+
}
42+
};
43+
44+
export const narrowToStringOrNumber = (str: string | number, someNumber: number) => {
45+
>narrowToStringOrNumber : Symbol(narrowToStringOrNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 14))
46+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41))
47+
>someNumber : Symbol(someNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 62))
48+
49+
switch (str) {
50+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41))
51+
52+
case 'abc': {
53+
// inferred type should be `abc`
54+
return str;
55+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41))
56+
}
57+
case someNumber: {
58+
>someNumber : Symbol(someNumber, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 62))
59+
60+
// inferred type should be `number`
61+
return str;
62+
>str : Symbol(str, Decl(switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts, 26, 41))
63+
}
64+
default:
65+
return 'defaultValue';
66+
}
67+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
=== tests/cases/compiler/switchCaseNarrowsMatchingClausesEvenWhenNonMatchingClausesExist.ts ===
2+
export const narrowToLiterals = (str: string) => {
3+
>narrowToLiterals : (str: string) => "abc" | "defaultValue"
4+
>(str: string) => { switch (str) { case 'abc': { // inferred type as `abc` return str; } default: return 'defaultValue'; } } : (str: string) => "abc" | "defaultValue"
5+
>str : string
6+
7+
switch (str) {
8+
>str : string
9+
10+
case 'abc': {
11+
>'abc' : "abc"
12+
13+
// inferred type as `abc`
14+
return str;
15+
>str : "abc"
16+
}
17+
default:
18+
return 'defaultValue';
19+
>'defaultValue' : "defaultValue"
20+
}
21+
};
22+
23+
export const narrowToString = (str: string, someOtherStr: string) => {
24+
>narrowToString : (str: string, someOtherStr: string) => string
25+
>(str: string, someOtherStr: string) => { switch (str) { case 'abc': { // inferred type should be `abc` return str; } case someOtherStr: { // `string` return str; } default: return 'defaultValue'; } } : (str: string, someOtherStr: string) => string
26+
>str : string
27+
>someOtherStr : string
28+
29+
switch (str) {
30+
>str : string
31+
32+
case 'abc': {
33+
>'abc' : "abc"
34+
35+
// inferred type should be `abc`
36+
return str;
37+
>str : "abc"
38+
}
39+
case someOtherStr: {
40+
>someOtherStr : string
41+
42+
// `string`
43+
return str;
44+
>str : string
45+
}
46+
default:
47+
return 'defaultValue';
48+
>'defaultValue' : "defaultValue"
49+
}
50+
};
51+
52+
export const narrowToStringOrNumber = (str: string | number, someNumber: number) => {
53+
>narrowToStringOrNumber : (str: string | number, someNumber: number) => number | "abc" | "defaultValue"
54+
>(str: string | number, someNumber: number) => { switch (str) { case 'abc': { // inferred type should be `abc` return str; } case someNumber: { // inferred type should be `number` return str; } default: return 'defaultValue'; } } : (str: string | number, someNumber: number) => number | "abc" | "defaultValue"
55+
>str : string | number
56+
>someNumber : number
57+
58+
switch (str) {
59+
>str : string | number
60+
61+
case 'abc': {
62+
>'abc' : "abc"
63+
64+
// inferred type should be `abc`
65+
return str;
66+
>str : "abc"
67+
}
68+
case someNumber: {
69+
>someNumber : number
70+
71+
// inferred type should be `number`
72+
return str;
73+
>str : number
74+
}
75+
default:
76+
return 'defaultValue';
77+
>'defaultValue' : "defaultValue"
78+
}
79+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export const narrowToLiterals = (str: string) => {
2+
switch (str) {
3+
case 'abc': {
4+
// inferred type as `abc`
5+
return str;
6+
}
7+
default:
8+
return 'defaultValue';
9+
}
10+
};
11+
12+
export const narrowToString = (str: string, someOtherStr: string) => {
13+
switch (str) {
14+
case 'abc': {
15+
// inferred type should be `abc`
16+
return str;
17+
}
18+
case someOtherStr: {
19+
// `string`
20+
return str;
21+
}
22+
default:
23+
return 'defaultValue';
24+
}
25+
};
26+
27+
export const narrowToStringOrNumber = (str: string | number, someNumber: number) => {
28+
switch (str) {
29+
case 'abc': {
30+
// inferred type should be `abc`
31+
return str;
32+
}
33+
case someNumber: {
34+
// inferred type should be `number`
35+
return str;
36+
}
37+
default:
38+
return 'defaultValue';
39+
}
40+
};

0 commit comments

Comments
 (0)