Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Dec 15, 2025

Exhaustiveness checking failed when an enum had exactly one member. The type would not narrow to never in the default case even when all cases were handled:

enum Single {
  VALUE = 'VALUE'
}

function test(x: Single) {
  switch (x) {
    case Single.VALUE:
      return 1;
  }
  const n: never = x; // Error before, works now
}

Changes:

  • Modified narrowTypeBySwitchOnDiscriminant in checker.ts to narrow non-union types to never when exhaustively handled in switch default case
  • Added test coverage for single-member enums, numeric enums, and literal types

Implementation:

After computing the default type, check if the input type is non-union, unit-like, and already handled in switch cases. Return never if conditions met. Applies only to switch default case, not general if/else blocks.

Fixes #23155

Original prompt

This section details on the original issue you should resolve

<issue_title>Exhaustiveness checking against an enum only works when the enum has >1 member.</issue_title>
<issue_description>
TypeScript Version: [email protected]

Search Terms: discriminated, exhaustiveness, type guard, narrowing

Code

// Legal action types for ValidAction
enum ActionTypes {
  INCREMENT = 'INCREMENT',
//   DECREMENT = 'DECREMENT',
}

interface IIncrement {
  payload: {};
  type: ActionTypes.INCREMENT;
}

// interface IDecrement {
//   payload: {};
//   type: ActionTypes.DECREMENT;
// }

// Any string not present in T
type AnyStringExcept<T extends string> = { [P in T]: never; };

// ValidAction is an interface with a type in ActionTypes
type ValidAction = IIncrement;
// type ValidAction = IIncrement | IDecrement;
// UnhandledAction in an interface with a type that is not within ActionTypes
type UnhandledAction = { type: AnyStringExcept<ActionTypes>; };

// The set of all actions
type PossibleAction = ValidAction | UnhandledAction;

// Discriminates to ValidAction
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) {
    // typeof action === PossibleAction
    if (isUnhandled(action)) {
        // typeof action === UnhandledAction
        return state;
    }

    // typeof action === ValidAction
    switch (action.type) {
        case ActionTypes.INCREMENT:
            // typeof action === IIncrement
            return state + 1;
        // case ActionTypes.DECREMENT:
        //     return state - 1;
    }

    // typeof action === IIncrement
    // Since INCREMENT is handled above, this should be impossible,
    // However the compiler will say that assertNever cannot receive an argument of type IIncrement
    return assertNever(action);
}

function assertNever(x: UnhandledAction): never {
    throw new Error(`Unhandled action type: ${x.type}`);
}

Expected behavior: No error would be thrown, as the switch statement is exhaustive. If the ActionTypes.DECREMENT parts are uncommented (resulting in two possible values for ActionTypes) there is no error. An error only occurs when ActionTypes takes on a single value. The error occurs even if the never assertion happens in the default statement, which is obviously unreachable from IIncrement.

Actual behavior: An error is thrown despite the only possible value being explicitly handled. If ActionTypes.DECREMENT is uncommented the expected behavior is present.

Playground Link: (fixed the links)
[Error](https://www.typescriptlang.org/play/index.html#src=%2F%2F%20Legal%20action%20types%20for%20ValidAction%0D%0Aenum%20ActionTypes%20%7B%0D%0A%20%20INCREMENT%20%3D%20'INCREMENT'%2C%0D%0A%2F%2F%20%20%20DECREMENT%20%3D%20'DECREMENT'%2C%0D%0A%7D%0D%0A%0D%0Ainterface%20IIncrement%20%7B%0D%0A%20%20payload%3A%20%7B%7D%3B%0D%0A%20%20type%3A%20ActionTypes.INCREMENT%3B%0D%0A%7D%0D%0A%0D%0A%2F%2F%20interface%20IDecrement%20%7B%0D%0A%2F%2F%20%20%20payload%3A%20%7B%7D%3B%0D%0A%2F%2F%20%20%20type%3A%20ActionTypes.DECREMENT%3B%0D%0A%2F%2F%20%7D%0D%0A%0D%0A%2F%2F%20Any%20string%20not%20present%20in%20T%0D%0Atype%20AnyStringExcept%3CT%20extends%20string%3E%20%3D%20%7B%20%5BP%20in%20T%5D%3A%20never%3B%20%7D%3B%0D%0A%0D%0A%2F%2F%20ValidAction%20is%20an%20interface%20with%20a%20type%20in%20ActionTypes%0D%0Atype%20ValidAction%20%3D%20IIncrement%3B%0D%0A%2F%2F%20type%20ValidAction%20%3D%20IIncrement%20%7C%20IDecrement%3B%0D%0A%0D%0A%2F%2F%20UnhandledAction%20in%20an%20interface%20with%20a%20type%20that%20is%20not%20within%20ActionTypes%0D%0Atype%20UnhandledAction%20%3D%20%7B%20type%3A%20AnyStringExcept%3CActionTypes%3E%3B%20%7D%3B%0D%0A%0D%0A%2F%2F%20The%20set%20of%20all%20actions%0D%0Atype%20PossibleAction%20%3D%20ValidAction%20%7C%20UnhandledAction%3B%0D%0A%0D%0A%2F%2F%20Discriminates%20to%20ValidAction%0D%0Afunction%20isUnhandled(x%3A%20PossibleAction)%3A%20x%20is%20UnhandledAction%20%7B%0D%0A%20%20%20%20return%20!(x.type%20in%20ActionTypes)%3B%0D%0A%7D%0D%0A%0D%0Atype%20CounterState%20%3D%20number%3B%0D%0Aconst%20initialState%3A%20CounterState%20%3D%200%3B%0D%0A%0D%0Afunction%20receiveAction(state%20%3D%20initialState%2C%20action%3A%20PossibleAction)%20%7B%0D%0A%20%20%20%20%2F%2F%20typeof%20action%20%3D%3D%3D%20PossibleAction%0D%0A%20%20%20%20if%20(isUnhandled(action))%20%7B%0D%0A%20%20%20%20%20%20%20%20%2F%2F%20typeof%20action%20%3D%3D%3D%20UnhandledAction%0D%0A%20%20%20%20%20%20%20%20return%20state%3B%0D%0A%20%20%20%20%7D%0D%0A%0D%0A%20%20%20%20%2F%2F%20typeof%20action%20%3D%3D%3D%20ValidActio...


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@typescript-bot typescript-bot added the For Milestone Bug PRs that fix a bug with a specific milestone label Dec 15, 2025
Copilot AI changed the title [WIP] Fix exhaustiveness checking against a single member enum Fix exhaustiveness checking for single-member enums in switch statements Dec 15, 2025
Copilot AI requested a review from RyanCavanaugh December 15, 2025 20:06
@rubiesonthesky
Copy link

The description links to wrong issue with Fixed comment. The correct issue is below it, but it was surprising to end up in an issue that was already closed and didn’t seem related.

@RyanCavanaugh
Copy link
Member

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 15, 2025

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results

@typescript-bot
Copy link
Collaborator

Hey @RyanCavanaugh, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh Here are the results of running the user tests with tsc comparing main and refs/pull/62900/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Git clone failed"

Otherwise...

Everything looks good!

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Errors 1 1 ~ ~ ~ p=1.000 n=6
Symbols 62,370 62,370 ~ ~ ~ p=1.000 n=6
Types 50,387 50,387 ~ ~ ~ p=1.000 n=6
Memory used 194,934k (± 0.97%) 194,266k (± 0.89%) ~ 192,461k 196,101k p=0.336 n=6
Parse Time 1.30s (± 0.58%) 1.31s (± 0.62%) ~ 1.29s 1.31s p=0.206 n=6
Bind Time 0.76s 0.76s ~ ~ ~ p=1.000 n=6
Check Time 9.85s (± 0.27%) 9.84s (± 0.11%) ~ 9.83s 9.85s p=0.663 n=6
Emit Time 2.72s (± 0.40%) 2.73s (± 0.59%) ~ 2.71s 2.75s p=0.187 n=6
Total Time 14.63s (± 0.16%) 14.63s (± 0.08%) ~ 14.62s 14.65s p=0.679 n=6
angular-1 - node (v18.15.0, x64)
Errors 2 2 ~ ~ ~ p=1.000 n=6
Symbols 955,823 955,823 ~ ~ ~ p=1.000 n=6
Types 415,853 415,853 ~ ~ ~ p=1.000 n=6
Memory used 1,253,926k (± 0.01%) 1,253,927k (± 0.00%) ~ 1,253,864k 1,254,025k p=0.689 n=6
Parse Time 6.50s (± 0.47%) 6.50s (± 0.57%) ~ 6.45s 6.56s p=0.627 n=6
Bind Time 1.95s (± 0.21%) 1.96s (± 0.38%) +0.01s (+ 0.68%) 1.95s 1.97s p=0.010 n=6
Check Time 32.43s (± 0.33%) 32.33s (± 0.07%) ~ 32.29s 32.36s p=0.077 n=6
Emit Time 14.99s (± 0.29%) 14.95s (± 0.52%) ~ 14.82s 15.03s p=0.469 n=6
Total Time 55.88s (± 0.27%) 55.74s (± 0.17%) ~ 55.59s 55.85s p=0.093 n=6
mui-docs - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 2,723,786 2,723,786 ~ ~ ~ p=1.000 n=6
Types 937,507 937,507 ~ ~ ~ p=1.000 n=6
Memory used 3,051,363k (± 0.00%) 3,051,335k (± 0.00%) ~ 3,051,270k 3,051,387k p=1.000 n=6
Parse Time 8.59s (± 0.23%) 8.59s (± 0.25%) ~ 8.57s 8.63s p=0.683 n=6
Bind Time 2.31s (± 0.35%) 2.31s (± 0.35%) ~ 2.30s 2.32s p=0.204 n=6
Check Time 93.13s (± 0.59%) 92.93s (± 0.52%) ~ 92.01s 93.35s p=0.149 n=6
Emit Time 0.31s (± 2.44%) 0.31s (± 2.60%) ~ 0.30s 0.32s p=0.306 n=6
Total Time 104.34s (± 0.51%) 104.14s (± 0.46%) ~ 103.23s 104.54s p=0.149 n=6
self-build-src - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,252,023 1,252,029 +6 (+ 0.00%) ~ ~ p=0.001 n=6
Types 259,854 259,855 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,387,271k (± 0.03%) 2,569,580k (±11.83%) ~ 2,386,793k 3,115,196k p=0.173 n=6
Parse Time 5.16s (± 0.84%) 5.17s (± 1.87%) ~ 5.07s 5.34s p=1.000 n=6
Bind Time 1.87s (± 0.62%) 1.86s (± 0.81%) ~ 1.83s 1.87s p=0.115 n=6
Check Time 35.48s (± 0.34%) 35.39s (± 0.86%) ~ 34.87s 35.80s p=0.689 n=6
Emit Time 3.01s (± 2.17%) 3.03s (± 1.97%) ~ 2.95s 3.10s p=0.471 n=6
Total Time 45.53s (± 0.36%) 45.45s (± 0.87%) ~ 44.93s 46.06s p=0.521 n=6
self-build-src-public-api - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 1,252,023 1,252,029 +6 (+ 0.00%) ~ ~ p=0.001 n=6
Types 259,854 259,855 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 2,940,928k (±12.73%) 3,182,506k (± 0.03%) ~ 3,181,008k 3,183,131k p=0.471 n=6
Parse Time 6.72s (± 1.67%) 6.82s (± 0.48%) ~ 6.76s 6.85s p=0.066 n=6
Bind Time 2.27s (± 0.82%) 2.30s (± 2.53%) ~ 2.24s 2.40s p=0.688 n=6
Check Time 43.03s (± 0.52%) 43.25s (± 0.26%) ~ 43.09s 43.39s p=0.066 n=6
Emit Time 3.46s (± 1.95%) 3.51s (± 2.43%) ~ 3.42s 3.64s p=0.230 n=6
Total Time 55.50s (± 0.62%) 55.87s (± 0.37%) ~ 55.55s 56.10s p=0.066 n=6
self-compiler - node (v18.15.0, x64)
Errors 0 0 ~ ~ ~ p=1.000 n=6
Symbols 264,680 264,686 +6 (+ 0.00%) ~ ~ p=0.001 n=6
Types 104,067 104,068 +1 (+ 0.00%) ~ ~ p=0.001 n=6
Memory used 442,931k (± 0.01%) 443,087k (± 0.02%) +157k (+ 0.04%) 442,944k 443,160k p=0.020 n=6
Parse Time 3.51s (± 1.03%) 3.51s (± 0.74%) ~ 3.48s 3.55s p=0.746 n=6
Bind Time 1.37s (± 0.59%) 1.38s (± 0.85%) ~ 1.36s 1.39s p=0.401 n=6
Check Time 19.15s (± 0.64%) 19.12s (± 0.34%) ~ 19.05s 19.22s p=0.378 n=6
Emit Time 1.54s (± 0.54%) 1.54s (± 0.68%) ~ 1.53s 1.56s p=0.109 n=6
Total Time 25.57s (± 0.40%) 25.55s (± 0.23%) ~ 25.47s 25.65s p=0.467 n=6
ts-pre-modules - node (v18.15.0, x64)
Errors 72 72 ~ ~ ~ p=1.000 n=6
Symbols 225,493 225,493 ~ ~ ~ p=1.000 n=6
Types 94,373 94,373 ~ ~ ~ p=1.000 n=6
Memory used 369,906k (± 0.05%) 369,811k (± 0.02%) ~ 369,722k 369,904k p=0.575 n=6
Parse Time 2.83s (± 0.82%) 2.84s (± 0.73%) ~ 2.80s 2.86s p=1.000 n=6
Bind Time 1.64s (± 0.99%) 1.65s (± 1.07%) ~ 1.62s 1.66s p=0.867 n=6
Check Time 16.62s (± 0.25%) 16.62s (± 0.18%) ~ 16.59s 16.67s p=0.746 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 21.10s (± 0.32%) 21.11s (± 0.20%) ~ 21.06s 21.17s p=0.808 n=6
vscode - node (v18.15.0, x64)
Errors 11 11 ~ ~ ~ p=1.000 n=6
Symbols 4,070,569 4,070,569 ~ ~ ~ p=1.000 n=6
Types 1,284,252 1,284,252 ~ ~ ~ p=1.000 n=6
Memory used 3,861,857k (± 0.00%) 3,861,960k (± 0.00%) ~ 3,861,810k 3,862,139k p=0.173 n=6
Parse Time 15.74s (± 0.64%) 15.69s (± 0.22%) ~ 15.63s 15.73s p=0.748 n=6
Bind Time 5.45s (± 2.62%) 5.31s (± 0.35%) ~ 5.28s 5.33s p=0.090 n=6
Check Time 113.35s (± 3.22%) 114.33s (± 3.52%) ~ 109.05s 118.87s p=0.810 n=6
Emit Time 41.55s (±15.64%) 43.75s (±18.65%) ~ 38.75s 59.86s p=0.336 n=6
Total Time 176.09s (± 5.74%) 179.08s (± 4.11%) ~ 171.05s 189.94s p=0.298 n=6
webpack - node (v18.15.0, x64)
Errors 41 41 ~ ~ ~ p=1.000 n=6
Symbols 380,718 380,718 ~ ~ ~ p=1.000 n=6
Types 166,796 166,796 ~ ~ ~ p=1.000 n=6
Memory used 539,256k (± 0.01%) 539,331k (± 0.02%) ~ 539,113k 539,459k p=0.230 n=6
Parse Time 4.72s (± 0.92%) 4.70s (± 0.31%) ~ 4.68s 4.72s p=0.195 n=6
Bind Time 2.05s (± 1.53%) 2.06s (± 1.55%) ~ 2.03s 2.12s p=0.419 n=6
Check Time 22.91s (± 0.34%) 23.10s (± 1.19%) ~ 22.86s 23.63s p=0.078 n=6
Emit Time 0.00s 0.00s ~ ~ ~ p=1.000 n=6
Total Time 29.68s (± 0.45%) 29.86s (± 0.85%) ~ 29.62s 30.33s p=0.230 n=6
xstate-main - node (v18.15.0, x64)
Errors 30 30 ~ ~ ~ p=1.000 n=6
Symbols 694,700 694,700 ~ ~ ~ p=1.000 n=6
Types 212,083 212,083 ~ ~ ~ p=1.000 n=6
Memory used 589,869k (± 0.03%) 589,847k (± 0.03%) ~ 589,643k 590,070k p=0.936 n=6
Parse Time 4.19s (± 0.54%) 4.20s (± 0.46%) ~ 4.17s 4.22s p=0.292 n=6
Bind Time 1.39s (± 1.15%) 1.40s (± 1.28%) ~ 1.37s 1.42s p=0.417 n=6
Check Time 21.31s (± 1.71%) 21.24s (± 1.53%) ~ 20.91s 21.63s p=0.936 n=6
Emit Time 0.00s (±244.70%) 0.01s (±167.16%) ~ 0.00s 0.02s p=0.527 n=6
Total Time 26.89s (± 1.46%) 26.84s (± 1.30%) ~ 26.48s 27.27s p=0.810 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

@RyanCavanaugh Here are the results of running the top 400 repos with tsc comparing main and refs/pull/62900/merge:

Everything looks good!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Milestone Bug PRs that fix a bug with a specific milestone

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exhaustiveness checking against an enum only works when the enum has >1 member. Type error in Buffer.from()

4 participants