Skip to content

Instantly share code, notes, and snippets.

@tijnjh
Last active June 22, 2026 10:29
Show Gist options
  • Select an option

  • Save tijnjh/e8b3c575f9813988577c3382d21558f7 to your computer and use it in GitHub Desktop.

Select an option

Save tijnjh/e8b3c575f9813988577c3382d21558f7 to your computer and use it in GitHub Desktop.
match.ts
type MatchableValue<O, K extends keyof O> = Extract<O[K], PropertyKey>;
type MatchObjectCases<O extends object, K extends keyof O> = Partial<{
[V in MatchableValue<O, K>]: (obj: Extract<O, Record<K, V>>) => unknown;
}> & { _?: (obj: O) => unknown };
export function match<V extends PropertyKey, C>(
value: V,
cases: C & { [K in keyof C]: C[K] extends () => unknown ? unknown : never } & ("_" extends keyof C
? Record<Exclude<keyof C, V | "_">, never>
: Record<Exclude<keyof C, V>, never> & Record<Exclude<V, keyof C>, never>),
_arg2?: never,
): C[keyof C] extends () => infer R ? R : never;
export function match<O extends object, K extends keyof O, const C extends MatchObjectCases<O, K>>(
obj: O,
key: K,
cases: C &
("_" extends keyof C
? Record<Exclude<keyof C, MatchableValue<O, K> | "_">, never>
: Record<Exclude<keyof C, MatchableValue<O, K>>, never> &
Record<Exclude<MatchableValue<O, K>, keyof C>, never>),
): { [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never }[keyof C];
// @ts-expect-error implicit any
export function match(arg0, arg1, arg2) {
const [value, cases, obj] = arg2 !== undefined ? [arg0[arg1], arg2, arg0] : [arg0, arg1];
return (cases[value] ?? cases._)(obj);
}
@tijnjh

tijnjh commented Jun 15, 2026

Copy link
Copy Markdown
Author
// -- testing --

declare const color: 'red' | 'green' | 'blue'
declare const status: 'idle' | 'loading' | 'error'
declare const size: 'sm' | 'md' | 'lg'
declare const n: 1 | 2 | 3
declare const maybe: 'yes' | 'no' | 'maybe'

// union / literals / exhaustive
match(color, {
  red: () => 0,
  green: () => 1,
  blue: () => 2
}) satisfies 0 | 1 | 2

// union / literals / fallback
match(color, {
  red: () => 0,
  _: () => 1
}) satisfies 0 | 1

// string union / string literals / exhaustive
match(status, {
  idle: () => 'waiting',
  loading: () => 'busy',
  error: () => 'failed'
}) satisfies 'waiting' | 'busy' | 'failed'

// string union / object literals / exhaustive
match(status, {
  idle: () => ({ state: 'idle' as const }),
  loading: () => ({ state: 'loading' as const }),
  error: () => ({ state: 'error' as const })
}) satisfies
| { state: 'idle' }
| { state: 'loading' }
| { state: 'error' }

// string union / object literals / fallback
match(status, {
  error: () => ({ ok: false as const }),
  _: () => ({ ok: true as const })
}) satisfies { ok: false } | { ok: true }

// numeric union / exhaustive
match(n, {
  1: () => 'one',
  2: () => 'two',
  3: () => 'three'
}) satisfies 'one' | 'two' | 'three'

// numeric union / fallback
match(n, {
  1: () => 'one',
  _: () => 'many'
}) satisfies 'one' | 'many'

// mixed return literals
match(size, {
  sm: () => 1,
  md: () => 'medium',
  lg: () => true
}) satisfies 1 | 'medium' | true

// nullish returns
match(maybe, {
  yes: () => true,
  no: () => false,
  maybe: () => null
}) satisfies true | false | null

// undefined return
match(maybe, {
  yes: () => 'yes',
  no: () => 'no',
  maybe: () => undefined
}) satisfies 'yes' | 'no' | undefined

// fallback can cover multiple missing cases
match(maybe, {
  yes: () => 1,
  _: () => 0
}) satisfies 1 | 0

// all fallback
match(color, {
  _: () => 'fallback'
}) satisfies 'fallback'

// readonly-ish literal arrays
match(color, {
  red: () => ['r'] as const,
  green: () => ['g'] as const,
  blue: () => ['b'] as const
})

// nested object literals
match(status, {
  idle: () => ({ type: 'idle' as const, canCancel: false as const }),
  loading: () => ({ type: 'loading' as const, canCancel: true as const }),
  error: () => ({ type: 'error' as const, canRetry: true as const })
}) satisfies
| { type: 'idle', canCancel: false }
| { type: 'loading', canCancel: true }
| { type: 'error', canRetry: true }

// literal input value
match('red', {
  red: () => 123
}) satisfies 123

// widened string with fallback
declare const widenedString: string

match(widenedString, {
  hello: () => 1,
  _: () => 0
}) satisfies 1 | 0

// widened number with fallback
declare const widenedNumber: number

match(widenedNumber, {
  1: () => 'one',
  _: () => 'other'
}) satisfies 'one' | 'other'

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