From a0f867228880ea6a858a590f1a1063ed7196f80a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:37:26 +0000 Subject: [PATCH 1/4] Initial plan From 425a2c086d2eefdca20b18f31cf281dd4060730e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:41:14 +0000 Subject: [PATCH 2/4] Initial investigation of exhaustiveness checking issue Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- test-issue.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test-issue.ts diff --git a/test-issue.ts b/test-issue.ts new file mode 100644 index 0000000000000..64ffd9c8012f7 --- /dev/null +++ b/test-issue.ts @@ -0,0 +1,49 @@ +// Test the issue: Exhaustiveness checking against an enum with 1 member + +enum ActionTypes { + INCREMENT = 'INCREMENT', +} + +interface IIncrement { + payload: {}; + type: ActionTypes.INCREMENT; +} + +type AnyStringExcept = { [P in T]: never; }; + +type ValidAction = IIncrement; +type UnhandledAction = { type: AnyStringExcept; }; +type PossibleAction = ValidAction | UnhandledAction; + +function isUnhandled(x: PossibleAction): x is UnhandledAction { + return !(x.type in ActionTypes); +} + +type CounterState = number; +const initialState: CounterState = 0; + +function receiveAction(state = initialState, action: PossibleAction) { + if (isUnhandled(action)) { + return state; + } + + // typeof action === ValidAction + switch (action.type) { + case ActionTypes.INCREMENT: + return state + 1; + } + + // This should not error - all cases are handled + const n: never = action; + return state; +} + +// Simpler test case from RyanCavanaugh's comment +function fn(obj: { name: "bob" }) { + if (obj.name == "bob") { + // bob case + } else { + // Should not be an error + const n: never = obj; + } +} From ca8861146549564ee60d700881c4944988410b44 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:47:34 +0000 Subject: [PATCH 3/4] Fix exhaustiveness checking for single-member enums in switch statements Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 7 + test-issue.ts | 49 ---- ...xhaustiveSwitchSingleEnumMember.errors.txt | 90 +++++++ .../exhaustiveSwitchSingleEnumMember.js | 163 +++++++++++ .../exhaustiveSwitchSingleEnumMember.symbols | 173 ++++++++++++ .../exhaustiveSwitchSingleEnumMember.types | 255 ++++++++++++++++++ .../exhaustiveSwitchSingleEnumMember.ts | 84 ++++++ 7 files changed, 772 insertions(+), 49 deletions(-) delete mode 100644 test-issue.ts create mode 100644 tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt create mode 100644 tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js create mode 100644 tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols create mode 100644 tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types create mode 100644 tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8c52e9d172877..cdea15b25e8cd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -29772,6 +29772,13 @@ 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 in the default case when all values are handled + if (!(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; + } + } return caseType.flags & TypeFlags.Never ? defaultType : getUnionType([caseType, defaultType]); } diff --git a/test-issue.ts b/test-issue.ts deleted file mode 100644 index 64ffd9c8012f7..0000000000000 --- a/test-issue.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Test the issue: Exhaustiveness checking against an enum with 1 member - -enum ActionTypes { - INCREMENT = 'INCREMENT', -} - -interface IIncrement { - payload: {}; - type: ActionTypes.INCREMENT; -} - -type AnyStringExcept = { [P in T]: never; }; - -type ValidAction = IIncrement; -type UnhandledAction = { type: AnyStringExcept; }; -type PossibleAction = ValidAction | UnhandledAction; - -function isUnhandled(x: PossibleAction): x is UnhandledAction { - return !(x.type in ActionTypes); -} - -type CounterState = number; -const initialState: CounterState = 0; - -function receiveAction(state = initialState, action: PossibleAction) { - if (isUnhandled(action)) { - return state; - } - - // typeof action === ValidAction - switch (action.type) { - case ActionTypes.INCREMENT: - return state + 1; - } - - // This should not error - all cases are handled - const n: never = action; - return state; -} - -// Simpler test case from RyanCavanaugh's comment -function fn(obj: { name: "bob" }) { - if (obj.name == "bob") { - // bob case - } else { - // Should not be an error - const n: never = obj; - } -} diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt new file mode 100644 index 0000000000000..ac907bcd4a039 --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt @@ -0,0 +1,90 @@ +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 is a more complex case + // that involves property access narrowing, not just direct value narrowing. + // This test focuses on direct value narrowing. + \ 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..0b82213e25421 --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js @@ -0,0 +1,163 @@ +//// [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 is a more complex case +// that involves property access narrowing, not just direct value narrowing. +// This test focuses on direct value narrowing. + + +//// [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 is a more complex case +// that involves property access narrowing, not just direct value narrowing. +// This test focuses on direct value narrowing. diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols new file mode 100644 index 0000000000000..cd47b9b45f1ba --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols @@ -0,0 +1,173 @@ +//// [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 is a more complex case +// that involves property access narrowing, not just direct value narrowing. +// This test focuses on direct value narrowing. + diff --git a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types new file mode 100644 index 0000000000000..9d9988a43115d --- /dev/null +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types @@ -0,0 +1,255 @@ +//// [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 is a more complex case +// that involves property access narrowing, not just direct value narrowing. +// This test focuses on direct value narrowing. + diff --git a/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts new file mode 100644 index 0000000000000..7ac2cfd826bec --- /dev/null +++ b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts @@ -0,0 +1,84 @@ +// @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 is a more complex case +// that involves property access narrowing, not just direct value narrowing. +// This test focuses on direct value narrowing. From 90c255548856b4f16c9c664983fd2824070a8e98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Dec 2025 19:29:12 +0000 Subject: [PATCH 4/4] Add narrowing for non-union types in narrowTypeByDiscriminant Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 20 +++++++++++++++++-- ...xhaustiveSwitchSingleEnumMember.errors.txt | 5 ++--- .../exhaustiveSwitchSingleEnumMember.js | 10 ++++------ .../exhaustiveSwitchSingleEnumMember.symbols | 5 ++--- .../exhaustiveSwitchSingleEnumMember.types | 5 ++--- .../exhaustiveSwitchSingleEnumMember.ts | 5 ++--- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cdea15b25e8cd..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,13 +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 in the default case when all values are handled - if (!(type.flags & TypeFlags.Union) && isUnitLikeType(type)) { + // 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 index ac907bcd4a039..149a4dc472b7b 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.errors.txt @@ -84,7 +84,6 @@ exhaustiveSwitchSingleEnumMember.ts(78,9): error TS2322: Type 'MultiMemberEnum.B !!! error TS2322: Type 'MultiMemberEnum.B' is not assignable to type 'never'. } - // Note: Discriminated union narrowing for single-member types is a more complex case - // that involves property access narrowing, not just direct value narrowing. - // This test focuses on direct value narrowing. + // 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 index 0b82213e25421..5b9567b07e29e 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.js @@ -81,9 +81,8 @@ function testIncomplete(x: MultiMemberEnum) { const n: never = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented. //// [exhaustiveSwitchSingleEnumMember.js] @@ -158,6 +157,5 @@ function testIncomplete(x) { // Should NOT narrow to never - B is not handled var n = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// 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 index cd47b9b45f1ba..62335d1a76f29 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.symbols @@ -167,7 +167,6 @@ function testIncomplete(x: MultiMemberEnum) { >x : Symbol(x, Decl(exhaustiveSwitchSingleEnumMember.ts, 71, 24)) } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// 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 index 9d9988a43115d..6a5d643664a5e 100644 --- a/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types +++ b/tests/baselines/reference/exhaustiveSwitchSingleEnumMember.types @@ -249,7 +249,6 @@ function testIncomplete(x: MultiMemberEnum) { > : ^^^^^^^^^^^^^^^^^ } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// 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 index 7ac2cfd826bec..f0de52108bfbe 100644 --- a/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts +++ b/tests/cases/compiler/exhaustiveSwitchSingleEnumMember.ts @@ -79,6 +79,5 @@ function testIncomplete(x: MultiMemberEnum) { const n: never = x; // Error expected } -// Note: Discriminated union narrowing for single-member types is a more complex case -// that involves property access narrowing, not just direct value narrowing. -// This test focuses on direct value narrowing. +// Note: Discriminated union narrowing for single-member types requires +// narrowing through property access, which is more complex and not yet implemented.