typedswitch
v1.0.0
Published
A type-safe, exhaustive switch expression for TypeScript discriminated unions and string literals
Maintainers
Readme
typedswitch
A type-safe, exhaustive switch expression for TypeScript. Handle discriminated unions and string literals with full type inference, compile-time exhaustiveness checking, and async support.
Installation
npm install typedswitchpnpm add typedswitchbun add typedswitchFeatures
- Full type inference — Return types are automatically inferred from your handlers
- Exhaustiveness checking — TypeScript ensures all cases are handled at compile time
- Discriminated union support — Works with any discriminant key, not just
type - Async-aware — Mixed sync/async handlers return
Promiseautomatically - Default handlers — Handle remaining cases with a fallback
- Return type constraints — Enforce that all handlers return a specific type
Usage
String/Enum Input
Switch on a string literal or enum value directly:
import { typedSwitch } from 'typedswitch'
type Status = 'success' | 'error' | 'pending'
const status: Status = 'success'
// All cases required — TypeScript enforces exhaustiveness
const message = typedSwitch(status, {
success: () => 'Operation completed!',
error: () => 'Something went wrong',
pending: () => 'Please wait...',
})
// message: stringEach handler receives the narrowed value:
const result = typedSwitch(status, {
success: (val) => val.toUpperCase(), // val: 'success'
error: (val) => val.toUpperCase(), // val: 'error'
pending: (val) => val.toUpperCase(), // val: 'pending'
})Discriminated Union Input
Switch on objects with a discriminant property (like type, kind, status, etc.):
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'scroll'; offset: number }
| { type: 'keypress'; key: string }
const event: Event = { type: 'click', x: 100, y: 200 }
// Specify the discriminant key as the second argument
const description = typedSwitch(event, 'type', {
click: (e) => `Clicked at (${e.x}, ${e.y})`, // e: { type: 'click'; x: number; y: number }
scroll: (e) => `Scrolled ${e.offset}px`, // e: { type: 'scroll'; offset: number }
keypress: (e) => `Pressed ${e.key}`, // e: { type: 'keypress'; key: string }
})
// description: stringWorks with any discriminant key:
type Order =
| { status: 'pending'; createdAt: Date }
| { status: 'completed'; completedAt: Date }
| { status: 'cancelled'; reason: string }
const order: Order = { status: 'completed', completedAt: new Date() }
const info = typedSwitch(order, 'status', {
pending: (o) => `Created: ${o.createdAt}`,
completed: (o) => `Done: ${o.completedAt}`,
cancelled: (o) => `Cancelled: ${o.reason}`,
})Default Handler
Handle a subset of cases and catch the rest with a default handler:
type Status = 'success' | 'error' | 'pending' | 'cancelled'
const status: Status = 'cancelled'
// Only handle specific cases, default handles the rest
const message = typedSwitch(
status,
{
success: () => 'Done!',
error: () => 'Failed!',
},
(val) => `Unhandled status: ${val}` // val: Status (full union)
)
// message: stringWorks with discriminated unions too:
const description = typedSwitch(
event,
'type',
{
click: (e) => `Clicked at (${e.x}, ${e.y})`,
},
(e) => `Unhandled event: ${e.type}` // e: Event (full union)
)Async Handlers
Mix sync and async handlers freely. If any handler returns a Promise, the result is automatically typed as Promise:
// All sync — returns string
const syncResult = typedSwitch(status, {
success: () => 'ok',
error: () => 'err',
pending: () => 'wait',
})
// syncResult: string
// Any async — returns Promise<string>
const asyncResult = typedSwitch(status, {
success: async () => {
const data = await fetchData()
return data.message
},
error: () => 'err', // sync handlers still work
pending: async () => 'wait',
})
// asyncResult: Promise<string>
const message = await asyncResultReturn Type Inference
Return types are inferred from all handlers as a union:
const result = typedSwitch(status, {
success: () => ({ ok: true, data: 'hello' }),
error: () => ({ ok: false, code: 500 }),
pending: () => null,
})
// result: { ok: true; data: string } | { ok: false; code: number } | nullReturn Type Constraints
Enforce that all handlers return a type extending a constraint using the curried form typedSwitch<Constraint>():
interface HasId {
id: string
}
// All handlers must return something with { id: string }
const result = typedSwitch<HasId>()(status, {
success: () => ({ id: '123', name: 'John' }), // ✓ OK
error: () => ({ id: '456', code: 500 }), // ✓ OK
pending: () => ({ id: '789' }), // ✓ OK
})
// result: { id: string; name: string } | { id: string; code: number } | { id: string }
// This would be a compile error:
typedSwitch<HasId>()(status, {
success: () => ({ name: 'John' }), // ✗ Error: missing 'id'
error: () => ({ id: '456' }),
pending: () => ({ id: '789' }),
})Works with discriminated unions and default handlers:
const result = typedSwitch<HasId>()(
event,
'type',
{
click: () => ({ id: 'click-1', x: 10, y: 20 }),
scroll: () => ({ id: 'scroll-1', offset: 100 }),
keypress: () => ({ id: 'key-1', key: 'Enter' }),
}
)Error Handling
When a case is unhandled at runtime (e.g., due to unsafe casts), typedSwitch throws a descriptive error:
const status = 'unknown' as Status
typedSwitch(status, {
success: () => 'ok',
error: () => 'err',
})
// Throws: "Unhandled case: unknown. Available cases: success, error"For discriminated unions:
// Throws: "Unhandled case: unknown (discriminant key: "type"). Available cases: click, scroll, keypress"API Reference
typedSwitch(value, cases)
Switch on a string literal with exhaustive case handling.
typedSwitch(value, cases, defaultHandler)
Switch on a string literal with partial cases and a default fallback.
typedSwitch(object, key, cases)
Switch on a discriminated union using the specified discriminant key.
typedSwitch(object, key, cases, defaultHandler)
Switch on a discriminated union with partial cases and a default fallback.
typedSwitch<Constraint>()
Returns a constrained version of typedSwitch that enforces all handlers return a type extending Constraint.
License
MIT
