Skip to content

Commit ca88611

Browse files
Fix exhaustiveness checking for single-member enums in switch statements
Co-authored-by: RyanCavanaugh <[email protected]>
1 parent 425a2c0 commit ca88611

File tree

7 files changed

+772
-49
lines changed

7 files changed

+772
-49
lines changed

src/compiler/checker.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29772,6 +29772,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2977229772
return caseType;
2977329773
}
2977429774
const defaultType = filterType(type, t => !(isUnitLikeType(t) && contains(switchTypes, t.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(t)), (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))));
29775+
// Allow non-union types to narrow to never in the default case when all values are handled
29776+
if (!(type.flags & TypeFlags.Union) && isUnitLikeType(type)) {
29777+
const regularType = type.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(type));
29778+
if (isUnitType(regularType) && contains(switchTypes, regularType, (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))) {
29779+
return neverType;
29780+
}
29781+
}
2977529782
return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]);
2977629783
}
2977729784

test-issue.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
exhaustiveSwitchSingleEnumMember.ts(78,9): error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'.
2+
3+
4+
==== exhaustiveSwitchSingleEnumMember.ts (1 errors) ====
5+
// Test exhaustiveness checking for single-member enums
6+
// Repro for #23155
7+
8+
// Single enum member should narrow to never in default case
9+
enum SingleMemberEnum {
10+
VALUE = 'VALUE'
11+
}
12+
13+
function testSingleEnumExhaustive(x: SingleMemberEnum) {
14+
switch (x) {
15+
case SingleMemberEnum.VALUE:
16+
return 1;
17+
}
18+
// x should be narrowed to never here
19+
const n: never = x;
20+
}
21+
22+
// With explicit default clause
23+
function testSingleEnumWithDefault(x: SingleMemberEnum) {
24+
switch (x) {
25+
case SingleMemberEnum.VALUE:
26+
return 1;
27+
default:
28+
// x should be narrowed to never in default
29+
const n: never = x;
30+
throw new Error("unreachable");
31+
}
32+
}
33+
34+
// Numeric enum
35+
enum NumericSingleMember {
36+
ONE = 1
37+
}
38+
39+
function testNumericSingleEnum(x: NumericSingleMember) {
40+
switch (x) {
41+
case NumericSingleMember.ONE:
42+
return 'one';
43+
}
44+
const n: never = x;
45+
}
46+
47+
// Test that non-enum single types also work
48+
type SingleLiteral = 'onlyValue';
49+
50+
function testSingleLiteral(x: SingleLiteral) {
51+
switch (x) {
52+
case 'onlyValue':
53+
return 1;
54+
}
55+
const n: never = x;
56+
}
57+
58+
// Ensure unions still work correctly (existing behavior)
59+
enum MultiMemberEnum {
60+
A = 'A',
61+
B = 'B'
62+
}
63+
64+
function testMultiEnum(x: MultiMemberEnum) {
65+
switch (x) {
66+
case MultiMemberEnum.A:
67+
return 1;
68+
case MultiMemberEnum.B:
69+
return 2;
70+
}
71+
// Should narrow to never
72+
const n: never = x;
73+
}
74+
75+
// Test incomplete coverage - should error
76+
function testIncomplete(x: MultiMemberEnum) {
77+
switch (x) {
78+
case MultiMemberEnum.A:
79+
return 1;
80+
}
81+
// Should NOT narrow to never - B is not handled
82+
const n: never = x; // Error expected
83+
~
84+
!!! error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'.
85+
}
86+
87+
// Note: Discriminated union narrowing for single-member types is a more complex case
88+
// that involves property access narrowing, not just direct value narrowing.
89+
// This test focuses on direct value narrowing.
90+
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//// [tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts] ////
2+
3+
//// [exhaustiveSwitchSingleEnumMember.ts]
4+
// Test exhaustiveness checking for single-member enums
5+
// Repro for #23155
6+
7+
// Single enum member should narrow to never in default case
8+
enum SingleMemberEnum {
9+
VALUE = 'VALUE'
10+
}
11+
12+
function testSingleEnumExhaustive(x: SingleMemberEnum) {
13+
switch (x) {
14+
case SingleMemberEnum.VALUE:
15+
return 1;
16+
}
17+
// x should be narrowed to never here
18+
const n: never = x;
19+
}
20+
21+
// With explicit default clause
22+
function testSingleEnumWithDefault(x: SingleMemberEnum) {
23+
switch (x) {
24+
case SingleMemberEnum.VALUE:
25+
return 1;
26+
default:
27+
// x should be narrowed to never in default
28+
const n: never = x;
29+
throw new Error("unreachable");
30+
}
31+
}
32+
33+
// Numeric enum
34+
enum NumericSingleMember {
35+
ONE = 1
36+
}
37+
38+
function testNumericSingleEnum(x: NumericSingleMember) {
39+
switch (x) {
40+
case NumericSingleMember.ONE:
41+
return 'one';
42+
}
43+
const n: never = x;
44+
}
45+
46+
// Test that non-enum single types also work
47+
type SingleLiteral = 'onlyValue';
48+
49+
function testSingleLiteral(x: SingleLiteral) {
50+
switch (x) {
51+
case 'onlyValue':
52+
return 1;
53+
}
54+
const n: never = x;
55+
}
56+
57+
// Ensure unions still work correctly (existing behavior)
58+
enum MultiMemberEnum {
59+
A = 'A',
60+
B = 'B'
61+
}
62+
63+
function testMultiEnum(x: MultiMemberEnum) {
64+
switch (x) {
65+
case MultiMemberEnum.A:
66+
return 1;
67+
case MultiMemberEnum.B:
68+
return 2;
69+
}
70+
// Should narrow to never
71+
const n: never = x;
72+
}
73+
74+
// Test incomplete coverage - should error
75+
function testIncomplete(x: MultiMemberEnum) {
76+
switch (x) {
77+
case MultiMemberEnum.A:
78+
return 1;
79+
}
80+
// Should NOT narrow to never - B is not handled
81+
const n: never = x; // Error expected
82+
}
83+
84+
// Note: Discriminated union narrowing for single-member types is a more complex case
85+
// that involves property access narrowing, not just direct value narrowing.
86+
// This test focuses on direct value narrowing.
87+
88+
89+
//// [exhaustiveSwitchSingleEnumMember.js]
90+
"use strict";
91+
// Test exhaustiveness checking for single-member enums
92+
// Repro for #23155
93+
// Single enum member should narrow to never in default case
94+
var SingleMemberEnum;
95+
(function (SingleMemberEnum) {
96+
SingleMemberEnum["VALUE"] = "VALUE";
97+
})(SingleMemberEnum || (SingleMemberEnum = {}));
98+
function testSingleEnumExhaustive(x) {
99+
switch (x) {
100+
case SingleMemberEnum.VALUE:
101+
return 1;
102+
}
103+
// x should be narrowed to never here
104+
var n = x;
105+
}
106+
// With explicit default clause
107+
function testSingleEnumWithDefault(x) {
108+
switch (x) {
109+
case SingleMemberEnum.VALUE:
110+
return 1;
111+
default:
112+
// x should be narrowed to never in default
113+
var n = x;
114+
throw new Error("unreachable");
115+
}
116+
}
117+
// Numeric enum
118+
var NumericSingleMember;
119+
(function (NumericSingleMember) {
120+
NumericSingleMember[NumericSingleMember["ONE"] = 1] = "ONE";
121+
})(NumericSingleMember || (NumericSingleMember = {}));
122+
function testNumericSingleEnum(x) {
123+
switch (x) {
124+
case NumericSingleMember.ONE:
125+
return 'one';
126+
}
127+
var n = x;
128+
}
129+
function testSingleLiteral(x) {
130+
switch (x) {
131+
case 'onlyValue':
132+
return 1;
133+
}
134+
var n = x;
135+
}
136+
// Ensure unions still work correctly (existing behavior)
137+
var MultiMemberEnum;
138+
(function (MultiMemberEnum) {
139+
MultiMemberEnum["A"] = "A";
140+
MultiMemberEnum["B"] = "B";
141+
})(MultiMemberEnum || (MultiMemberEnum = {}));
142+
function testMultiEnum(x) {
143+
switch (x) {
144+
case MultiMemberEnum.A:
145+
return 1;
146+
case MultiMemberEnum.B:
147+
return 2;
148+
}
149+
// Should narrow to never
150+
var n = x;
151+
}
152+
// Test incomplete coverage - should error
153+
function testIncomplete(x) {
154+
switch (x) {
155+
case MultiMemberEnum.A:
156+
return 1;
157+
}
158+
// Should NOT narrow to never - B is not handled
159+
var n = x; // Error expected
160+
}
161+
// Note: Discriminated union narrowing for single-member types is a more complex case
162+
// that involves property access narrowing, not just direct value narrowing.
163+
// This test focuses on direct value narrowing.

0 commit comments

Comments
 (0)