ok this one drove me insane until i figured out what was happening lol **the short answer:** when the union comes from a generated/imported type file, TS sometimes sees the member types as *open interfaces* instead of *closed literals* — and open interfaces can be extended, so TS refuses to call the default branch `never` because technically something else could satisfy the type. **the actual fix:** make sure your union members are `type` aliases with literal discriminants, not `interface` declarations: ```typescript // ❌ this can cause the problem — interfaces are open/extendable interface ClickEvent { type: 'click'; x: number; y: number } interface KeyEvent { type: 'keypress'; key: string } type Event = ClickEvent | KeyEvent // ✅ this works reliably — type literals are closed type Event = | { type: 'click'; x: number; y: number } | { type: 'keypress'; key: string } | { type: 'scroll'; delta: number } ``` **the other culprit** (especially with generated types): if the generated file uses `export interface Foo extends Bar`, that `extends` means TS can't prove the union is exhaustive because `Bar` might add more members at declaration-merge time. **for cross-module exhaustiveness that's actually reliable**, the `satisfies never` pattern is slightly more explicit and surfaces better errors: ```typescript default: { const _: never = event // will error when non-exhaustive throw new Error(`unhandled event: ${(_ as any).type}`) } ``` if the never check is failing *silently* (no error even when you're missing cases), that's almost always the interface vs type problem. swap to inline type literals and it'll snap back into place 👍
bf6f5272-de82-41bd-a45c-b73c4358caff
ok this one drove me insane until i figured out what was happening lol
the short answer: when the union comes from a generated/imported type file, TS sometimes sees the member types as open interfaces instead of closed literals — and open interfaces can be extended, so TS refuses to call the default branch never because technically something else could satisfy the type.
the actual fix: make sure your union members are type aliases with literal discriminants, not interface declarations:
// ❌ this can cause the problem — interfaces are open/extendable
interface ClickEvent { type: 'click'; x: number; y: number }
interface KeyEvent { type: 'keypress'; key: string }
type Event = ClickEvent | KeyEvent
// ✅ this works reliably — type literals are closed
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'keypress'; key: string }
| { type: 'scroll'; delta: number }the other culprit (especially with generated types): if the generated file uses export interface Foo extends Bar, that extends means TS can't prove the union is exhaustive because Bar might add more members at declaration-merge time.
for cross-module exhaustiveness that's actually reliable, the satisfies never pattern is slightly more explicit and surfaces better errors:
default: {
const _: never = event // will error when non-exhaustive
throw new Error(`unhandled event: ${(_ as any).type}`)
}if the never check is failing silently (no error even when you're missing cases), that's almost always the interface vs type problem. swap to inline type literals and it'll snap back into place 👍