diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8c52e9d172877..f750b81094947 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29459,6 +29459,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } propType = removeNullable && optionalChain ? getOptionalType(propType) : propType; const narrowedPropType = narrowType(propType); + // If the narrowed property type is never and the type is not a union, return never + // This handles cases where a non-union type has a single discriminant value that's been exhausted + if (narrowedPropType.flags & TypeFlags.Never && !(type.flags & TypeFlags.Union)) { + return neverType; + } return filterType(type, t => { const discriminantType = getTypeOfPropertyOrIndexSignatureOfType(t, propName) || unknownType; return !(discriminantType.flags & TypeFlags.Never) && !(narrowedPropType.flags & TypeFlags.Never) && areTypesComparable(narrowedPropType, discriminantType); @@ -29772,6 +29777,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return caseType; } const defaultType = filterType(type, t => !(isUnitLikeType(t) && contains(switchTypes, t.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(t)), (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2)))); + // Allow non-union types to narrow to never when all values are handled + // This applies when caseType is never (meaning we're in a default-like position) + if (caseType.flags & TypeFlags.Never && !(type.flags & TypeFlags.Union) && isUnitLikeType(type)) { + const regularType = type.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(type)); + if (isUnitType(regularType) && contains(switchTypes, regularType, (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))) { + return neverType; + } + } + // Also handle single-member unions + if (caseType.flags & TypeFlags.Never && type.flags & TypeFlags.Union && (type as UnionType).types.length === 1) { + const singleType = (type as UnionType).types[0]; + if (isUnitLikeType(singleType)) { + const regularType = singleType.flags & TypeFlags.Undefined ? undefinedType : getRegularTypeOfLiteralType(extractUnitType(singleType)); + if (isUnitType(regularType) && contains(switchTypes, regularType, (t1, t2) => isUnitType(t1) && areTypesComparable(t1, t2))) { + return neverType; + } + } + } return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt new file mode 100644 index 0000000000000..149a4dc472b7b --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt @@ -0,0 +1,89 @@ +exhaustiveSwitchSingleEnumMember.ts(78,9): error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'. + + +==== exhaustiveSwitchSingleEnumMember.ts (1 errors) ==== + // Test exhaustiveness checking for single-member enums + // Repro for #23155 + + // Single enum member should narrow to never in default case + enum SingleMemberEnum { + VALUE = 'VALUE' + } + + function testSingleEnumExhaustive(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + } + // x should be narrowed to never here + const n: never = x; + } + + // With explicit default clause + function testSingleEnumWithDefault(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + default: + // x should be narrowed to never in default + const n: never = x; + throw new Error("unreachable"); + } + } + + // Numeric enum + enum NumericSingleMember { + ONE = 1 + } + + function testNumericSingleEnum(x: NumericSingleMember) { + switch (x) { + case NumericSingleMember.ONE: + return 'one'; + } + const n: never = x; + } + + // Test that non-enum single types also work + type SingleLiteral = 'onlyValue'; + + function testSingleLiteral(x: SingleLiteral) { + switch (x) { + case 'onlyValue': + return 1; + } + const n: never = x; + } + + // Ensure unions still work correctly (existing behavior) + enum MultiMemberEnum { + A = 'A', + B = 'B' + } + + function testMultiEnum(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + case MultiMemberEnum.B: + return 2; + } + // Should narrow to never + const n: never = x; + } + + // Test incomplete coverage - should error + function testIncomplete(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + } + // Should NOT narrow to never - B is not handled + const n: never = x; // Error expected + ~ +!!! error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'. + } + + // Note: Discriminated union narrowing for single-member types requires + // narrowing through property access, which is more complex and not yet implemented. + \ No newline at end of file diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js new file mode 100644 index 0000000000000..5b9567b07e29e --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js @@ -0,0 +1,161 @@ +//// [tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts] //// + +//// [exhaustiveSwitchSingleEnumMember.ts] +// Test exhaustiveness checking for single-member enums +// Repro for #23155 + +// Single enum member should narrow to never in default case +enum SingleMemberEnum { + VALUE = 'VALUE' +} + +function testSingleEnumExhaustive(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + } + // x should be narrowed to never here + const n: never = x; +} + +// With explicit default clause +function testSingleEnumWithDefault(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + default: + // x should be narrowed to never in default + const n: never = x; + throw new Error("unreachable"); + } +} + +// Numeric enum +enum NumericSingleMember { + ONE = 1 +} + +function testNumericSingleEnum(x: NumericSingleMember) { + switch (x) { + case NumericSingleMember.ONE: + return 'one'; + } + const n: never = x; +} + +// Test that non-enum single types also work +type SingleLiteral = 'onlyValue'; + +function testSingleLiteral(x: SingleLiteral) { + switch (x) { + case 'onlyValue': + return 1; + } + const n: never = x; +} + +// Ensure unions still work correctly (existing behavior) +enum MultiMemberEnum { + A = 'A', + B = 'B' +} + +function testMultiEnum(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + case MultiMemberEnum.B: + return 2; + } + // Should narrow to never + const n: never = x; +} + +// Test incomplete coverage - should error +function testIncomplete(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + } + // Should NOT narrow to never - B is not handled + const n: never = x; // Error expected +} + +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. + + +//// [exhaustiveSwitchSingleEnumMember.js] +"use strict"; +// Test exhaustiveness checking for single-member enums +// Repro for #23155 +// Single enum member should narrow to never in default case +var SingleMemberEnum; +(function (SingleMemberEnum) { + SingleMemberEnum["VALUE"] = "VALUE"; +})(SingleMemberEnum || (SingleMemberEnum = {})); +function testSingleEnumExhaustive(x) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + } + // x should be narrowed to never here + var n = x; +} +// With explicit default clause +function testSingleEnumWithDefault(x) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + default: + // x should be narrowed to never in default + var n = x; + throw new Error("unreachable"); + } +} +// Numeric enum +var NumericSingleMember; +(function (NumericSingleMember) { + NumericSingleMember[NumericSingleMember["ONE"] = 1] = "ONE"; +})(NumericSingleMember || (NumericSingleMember = {})); +function testNumericSingleEnum(x) { + switch (x) { + case NumericSingleMember.ONE: + return 'one'; + } + var n = x; +} +function testSingleLiteral(x) { + switch (x) { + case 'onlyValue': + return 1; + } + var n = x; +} +// Ensure unions still work correctly (existing behavior) +var MultiMemberEnum; +(function (MultiMemberEnum) { + MultiMemberEnum["A"] = "A"; + MultiMemberEnum["B"] = "B"; +})(MultiMemberEnum || (MultiMemberEnum = {})); +function testMultiEnum(x) { + switch (x) { + case MultiMemberEnum.A: + return 1; + case MultiMemberEnum.B: + return 2; + } + // Should narrow to never + var n = x; +} +// Test incomplete coverage - should error +function testIncomplete(x) { + switch (x) { + case MultiMemberEnum.A: + return 1; + } + // Should NOT narrow to never - B is not handled + var n = x; // Error expected +} +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols new file mode 100644 index 0000000000000..62335d1a76f29 --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols @@ -0,0 +1,172 @@ +//// [tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts] //// + +=== exhaustiveSwitchSingleEnumMember.ts === +// Test exhaustiveness checking for single-member enums +// Repro for #23155 + +// Single enum member should narrow to never in default case +enum SingleMemberEnum { +>SingleMemberEnum : Symbol(SingleMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 0, 0)) + + VALUE = 'VALUE' +>VALUE : Symbol(SingleMemberEnum.VALUE, Decl(exhaustiveSwitchSingleEnumMember.ts, 4, 23)) +} + +function testSingleEnumExhaustive(x: SingleMemberEnum) { +>testSingleEnumExhaustive : Symbol(testSingleEnumExhaustive, Decl(exhaustiveSwitchSingleEnumMember.ts, 6, 1)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 8, 34)) +>SingleMemberEnum : Symbol(SingleMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 0, 0)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 8, 34)) + + case SingleMemberEnum.VALUE: +>SingleMemberEnum.VALUE : Symbol(SingleMemberEnum.VALUE, Decl(exhaustiveSwitchSingleEnumMember.ts, 4, 23)) +>SingleMemberEnum : Symbol(SingleMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 0, 0)) +>VALUE : Symbol(SingleMemberEnum.VALUE, Decl(exhaustiveSwitchSingleEnumMember.ts, 4, 23)) + + return 1; + } + // x should be narrowed to never here + const n: never = x; +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 14, 7)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 8, 34)) +} + +// With explicit default clause +function testSingleEnumWithDefault(x: SingleMemberEnum) { +>testSingleEnumWithDefault : Symbol(testSingleEnumWithDefault, Decl(exhaustiveSwitchSingleEnumMember.ts, 15, 1)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 18, 35)) +>SingleMemberEnum : Symbol(SingleMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 0, 0)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 18, 35)) + + case SingleMemberEnum.VALUE: +>SingleMemberEnum.VALUE : Symbol(SingleMemberEnum.VALUE, Decl(exhaustiveSwitchSingleEnumMember.ts, 4, 23)) +>SingleMemberEnum : Symbol(SingleMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 0, 0)) +>VALUE : Symbol(SingleMemberEnum.VALUE, Decl(exhaustiveSwitchSingleEnumMember.ts, 4, 23)) + + return 1; + default: + // x should be narrowed to never in default + const n: never = x; +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 24, 11)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 18, 35)) + + throw new Error("unreachable"); +>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --)) + } +} + +// Numeric enum +enum NumericSingleMember { +>NumericSingleMember : Symbol(NumericSingleMember, Decl(exhaustiveSwitchSingleEnumMember.ts, 27, 1)) + + ONE = 1 +>ONE : Symbol(NumericSingleMember.ONE, Decl(exhaustiveSwitchSingleEnumMember.ts, 30, 26)) +} + +function testNumericSingleEnum(x: NumericSingleMember) { +>testNumericSingleEnum : Symbol(testNumericSingleEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 32, 1)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 34, 31)) +>NumericSingleMember : Symbol(NumericSingleMember, Decl(exhaustiveSwitchSingleEnumMember.ts, 27, 1)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 34, 31)) + + case NumericSingleMember.ONE: +>NumericSingleMember.ONE : Symbol(NumericSingleMember.ONE, Decl(exhaustiveSwitchSingleEnumMember.ts, 30, 26)) +>NumericSingleMember : Symbol(NumericSingleMember, Decl(exhaustiveSwitchSingleEnumMember.ts, 27, 1)) +>ONE : Symbol(NumericSingleMember.ONE, Decl(exhaustiveSwitchSingleEnumMember.ts, 30, 26)) + + return 'one'; + } + const n: never = x; +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 39, 7)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 34, 31)) +} + +// Test that non-enum single types also work +type SingleLiteral = 'onlyValue'; +>SingleLiteral : Symbol(SingleLiteral, Decl(exhaustiveSwitchSingleEnumMember.ts, 40, 1)) + +function testSingleLiteral(x: SingleLiteral) { +>testSingleLiteral : Symbol(testSingleLiteral, Decl(exhaustiveSwitchSingleEnumMember.ts, 43, 33)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 45, 27)) +>SingleLiteral : Symbol(SingleLiteral, Decl(exhaustiveSwitchSingleEnumMember.ts, 40, 1)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 45, 27)) + + case 'onlyValue': + return 1; + } + const n: never = x; +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 50, 7)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 45, 27)) +} + +// Ensure unions still work correctly (existing behavior) +enum MultiMemberEnum { +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) + + A = 'A', +>A : Symbol(MultiMemberEnum.A, Decl(exhaustiveSwitchSingleEnumMember.ts, 54, 22)) + + B = 'B' +>B : Symbol(MultiMemberEnum.B, Decl(exhaustiveSwitchSingleEnumMember.ts, 55, 10)) +} + +function testMultiEnum(x: MultiMemberEnum) { +>testMultiEnum : Symbol(testMultiEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 57, 1)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 59, 23)) +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 59, 23)) + + case MultiMemberEnum.A: +>MultiMemberEnum.A : Symbol(MultiMemberEnum.A, Decl(exhaustiveSwitchSingleEnumMember.ts, 54, 22)) +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) +>A : Symbol(MultiMemberEnum.A, Decl(exhaustiveSwitchSingleEnumMember.ts, 54, 22)) + + return 1; + case MultiMemberEnum.B: +>MultiMemberEnum.B : Symbol(MultiMemberEnum.B, Decl(exhaustiveSwitchSingleEnumMember.ts, 55, 10)) +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) +>B : Symbol(MultiMemberEnum.B, Decl(exhaustiveSwitchSingleEnumMember.ts, 55, 10)) + + return 2; + } + // Should narrow to never + const n: never = x; +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 67, 7)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 59, 23)) +} + +// Test incomplete coverage - should error +function testIncomplete(x: MultiMemberEnum) { +>testIncomplete : Symbol(testIncomplete, Decl(exhaustiveSwitchSingleEnumMember.ts, 68, 1)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 71, 24)) +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) + + switch (x) { +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 71, 24)) + + case MultiMemberEnum.A: +>MultiMemberEnum.A : Symbol(MultiMemberEnum.A, Decl(exhaustiveSwitchSingleEnumMember.ts, 54, 22)) +>MultiMemberEnum : Symbol(MultiMemberEnum, Decl(exhaustiveSwitchSingleEnumMember.ts, 51, 1)) +>A : Symbol(MultiMemberEnum.A, Decl(exhaustiveSwitchSingleEnumMember.ts, 54, 22)) + + return 1; + } + // Should NOT narrow to never - B is not handled + const n: never = x; // Error expected +>n : Symbol(n, Decl(exhaustiveSwitchSingleEnumMember.ts, 77, 7)) +>x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 71, 24)) +} + +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. + diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types new file mode 100644 index 0000000000000..6a5d643664a5e --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types @@ -0,0 +1,254 @@ +//// [tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts] //// + +=== exhaustiveSwitchSingleEnumMember.ts === +// Test exhaustiveness checking for single-member enums +// Repro for #23155 + +// Single enum member should narrow to never in default case +enum SingleMemberEnum { +>SingleMemberEnum : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + VALUE = 'VALUE' +>VALUE : SingleMemberEnum.VALUE +> : ^^^^^^^^^^^^^^^^^^^^^^ +>'VALUE' : "VALUE" +> : ^^^^^^^ +} + +function testSingleEnumExhaustive(x: SingleMemberEnum) { +>testSingleEnumExhaustive : (x: SingleMemberEnum) => number +> : ^ ^^ ^^^^^^^^^^^ +>x : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + switch (x) { +>x : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + case SingleMemberEnum.VALUE: +>SingleMemberEnum.VALUE : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ +>SingleMemberEnum : typeof SingleMemberEnum +> : ^^^^^^^^^^^^^^^^^^^^^^^ +>VALUE : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + // x should be narrowed to never here + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ +} + +// With explicit default clause +function testSingleEnumWithDefault(x: SingleMemberEnum) { +>testSingleEnumWithDefault : (x: SingleMemberEnum) => number +> : ^ ^^ ^^^^^^^^^^^ +>x : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + switch (x) { +>x : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + case SingleMemberEnum.VALUE: +>SingleMemberEnum.VALUE : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ +>SingleMemberEnum : typeof SingleMemberEnum +> : ^^^^^^^^^^^^^^^^^^^^^^^ +>VALUE : SingleMemberEnum +> : ^^^^^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + + default: + // x should be narrowed to never in default + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ + + throw new Error("unreachable"); +>new Error("unreachable") : Error +> : ^^^^^ +>Error : ErrorConstructor +> : ^^^^^^^^^^^^^^^^ +>"unreachable" : "unreachable" +> : ^^^^^^^^^^^^^ + } +} + +// Numeric enum +enum NumericSingleMember { +>NumericSingleMember : NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^ + + ONE = 1 +>ONE : NumericSingleMember.ONE +> : ^^^^^^^^^^^^^^^^^^^^^^^ +>1 : 1 +> : ^ +} + +function testNumericSingleEnum(x: NumericSingleMember) { +>testNumericSingleEnum : (x: NumericSingleMember) => string +> : ^ ^^ ^^^^^^^^^^^ +>x : NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^ + + switch (x) { +>x : NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^ + + case NumericSingleMember.ONE: +>NumericSingleMember.ONE : NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^ +>NumericSingleMember : typeof NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>ONE : NumericSingleMember +> : ^^^^^^^^^^^^^^^^^^^ + + return 'one'; +>'one' : "one" +> : ^^^^^ + } + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ +} + +// Test that non-enum single types also work +type SingleLiteral = 'onlyValue'; +>SingleLiteral : "onlyValue" +> : ^^^^^^^^^^^ + +function testSingleLiteral(x: SingleLiteral) { +>testSingleLiteral : (x: SingleLiteral) => number +> : ^ ^^ ^^^^^^^^^^^ +>x : "onlyValue" +> : ^^^^^^^^^^^ + + switch (x) { +>x : "onlyValue" +> : ^^^^^^^^^^^ + + case 'onlyValue': +>'onlyValue' : "onlyValue" +> : ^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ +} + +// Ensure unions still work correctly (existing behavior) +enum MultiMemberEnum { +>MultiMemberEnum : MultiMemberEnum +> : ^^^^^^^^^^^^^^^ + + A = 'A', +>A : MultiMemberEnum.A +> : ^^^^^^^^^^^^^^^^^ +>'A' : "A" +> : ^^^ + + B = 'B' +>B : MultiMemberEnum.B +> : ^^^^^^^^^^^^^^^^^ +>'B' : "B" +> : ^^^ +} + +function testMultiEnum(x: MultiMemberEnum) { +>testMultiEnum : (x: MultiMemberEnum) => 1 | 2 +> : ^ ^^ ^^^^^^^^^^ +>x : MultiMemberEnum +> : ^^^^^^^^^^^^^^^ + + switch (x) { +>x : MultiMemberEnum +> : ^^^^^^^^^^^^^^^ + + case MultiMemberEnum.A: +>MultiMemberEnum.A : MultiMemberEnum.A +> : ^^^^^^^^^^^^^^^^^ +>MultiMemberEnum : typeof MultiMemberEnum +> : ^^^^^^^^^^^^^^^^^^^^^^ +>A : MultiMemberEnum.A +> : ^^^^^^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + + case MultiMemberEnum.B: +>MultiMemberEnum.B : MultiMemberEnum.B +> : ^^^^^^^^^^^^^^^^^ +>MultiMemberEnum : typeof MultiMemberEnum +> : ^^^^^^^^^^^^^^^^^^^^^^ +>B : MultiMemberEnum.B +> : ^^^^^^^^^^^^^^^^^ + + return 2; +>2 : 2 +> : ^ + } + // Should narrow to never + const n: never = x; +>n : never +> : ^^^^^ +>x : never +> : ^^^^^ +} + +// Test incomplete coverage - should error +function testIncomplete(x: MultiMemberEnum) { +>testIncomplete : (x: MultiMemberEnum) => 1 | undefined +> : ^ ^^ ^^^^^^^^^^^^^^^^^^ +>x : MultiMemberEnum +> : ^^^^^^^^^^^^^^^ + + switch (x) { +>x : MultiMemberEnum +> : ^^^^^^^^^^^^^^^ + + case MultiMemberEnum.A: +>MultiMemberEnum.A : MultiMemberEnum.A +> : ^^^^^^^^^^^^^^^^^ +>MultiMemberEnum : typeof MultiMemberEnum +> : ^^^^^^^^^^^^^^^^^^^^^^ +>A : MultiMemberEnum.A +> : ^^^^^^^^^^^^^^^^^ + + return 1; +>1 : 1 +> : ^ + } + // Should NOT narrow to never - B is not handled + const n: never = x; // Error expected +>n : never +> : ^^^^^ +>x : MultiMemberEnum.B +> : ^^^^^^^^^^^^^^^^^ +} + +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. + diff --git a/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts new file mode 100644 index 0000000000000..f0de52108bfbe --- /dev/null +++ b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts @@ -0,0 +1,83 @@ +// @strict: true +// Test exhaustiveness checking for single-member enums +// Repro for #23155 + +// Single enum member should narrow to never in default case +enum SingleMemberEnum { + VALUE = 'VALUE' +} + +function testSingleEnumExhaustive(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + } + // x should be narrowed to never here + const n: never = x; +} + +// With explicit default clause +function testSingleEnumWithDefault(x: SingleMemberEnum) { + switch (x) { + case SingleMemberEnum.VALUE: + return 1; + default: + // x should be narrowed to never in default + const n: never = x; + throw new Error("unreachable"); + } +} + +// Numeric enum +enum NumericSingleMember { + ONE = 1 +} + +function testNumericSingleEnum(x: NumericSingleMember) { + switch (x) { + case NumericSingleMember.ONE: + return 'one'; + } + const n: never = x; +} + +// Test that non-enum single types also work +type SingleLiteral = 'onlyValue'; + +function testSingleLiteral(x: SingleLiteral) { + switch (x) { + case 'onlyValue': + return 1; + } + const n: never = x; +} + +// Ensure unions still work correctly (existing behavior) +enum MultiMemberEnum { + A = 'A', + B = 'B' +} + +function testMultiEnum(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + case MultiMemberEnum.B: + return 2; + } + // Should narrow to never + const n: never = x; +} + +// Test incomplete coverage - should error +function testIncomplete(x: MultiMemberEnum) { + switch (x) { + case MultiMemberEnum.A: + return 1; + } + // Should NOT narrow to never - B is not handled + const n: never = x; // Error expected +} + +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented.