Skip to main content

conditional

Type-level branching — If, IfExtends, Switch, Match.

import type { If, IfExtends, Switch, Match } from '@gentleduck/ttest/conditional'

Type-level branching primitives. If and IfExtends cover the two-way case; Switch keys into a record of cases; Match does ordered pattern matching against [When, Then] tuples.

If

type If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else

Type-level ternary on a boolean condition.

type A = If<true,  'yes', 'no'>  // 'yes'
type B = If<false, 'yes', 'no'>  // 'no'
import type { IsString } from '@gentleduck/ttest/guard'
type Render<T> = If<IsString<T>, `[${T & string}]`, never>
type a = Render<'foo'>   // '[foo]'
type b = Render<number>  // never

Edge case: If<boolean, A, B> is A | Bboolean distributes through both branches. Pin to true or false first.

IfExtends

type IfExtends<A, B, Then, Else> = A extends B ? Then : Else

Branch on assignability rather than on a precomputed boolean.

type A = IfExtends<'admin', 'admin' | 'user', true, false>  // true
type B = IfExtends<string,  number,            true, false> // false
type Trim<S extends string> = IfExtends<S, ` ${infer R}`, Trim<R>, S>
type a = Trim<'   hi'>  // 'hi'

Edge case: IfExtends distributes over unions in A. IfExtends<'a' | 'b', 'a', 1, 0> is 1 | 0. Wrap in [A] extends [B] if you want the no-distribute version — see equality ShallowEqual for the standard pattern.

Switch

type Switch<Value extends PropertyKey, Cases, Default = never>

Match Value against a record of Case => Result; falls back to Default.

type Status = Switch<'pending', { pending: 1; done: 2; error: 3 }, 0>  // 1
type Miss   = Switch<'other',   { pending: 1; done: 2 }, 0>            // 0
// Map an HTTP status family to a sentinel.
type Label<S extends 'success' | 'redirect' | 'error'> = Switch<S, {
  success:  'ok'
  redirect: '3xx'
  error:    'fail'
}>
type a = Label<'success'>   // 'ok'
type b = Label<'redirect'>  // '3xx'

Edge case: Value must extend PropertyKey (string | number | symbol). For non-key values use Match.

Match

type Match<Value, Cases extends readonly (readonly [unknown, unknown])[], Default = never>

Match Value against ordered [When, Then] tuples. First-match wins.

type Label<T> = Match<T, [
  [string,  'str'],
  [number,  'num'],
  [boolean, 'bool'],
], 'other'>

type a = Label<'hi'>  // 'str'
type b = Label<42>    // 'num'
type c = Label<true>  // 'bool'
type d = Label<null>  // 'other'
// Ordering matters — narrower cases first.
type ChainOrder<T> = Match<T, [
  [readonly [],         'empty'],
  [readonly [unknown],  'single'],
  [readonly unknown[],  'many'],
], 'not-array'>
type a = ChainOrder<readonly []>          // 'empty'
type b = ChainOrder<readonly [1]>         // 'single'
type c = ChainOrder<readonly [1, 2, 3]>   // 'many'

Edge case: deep Match chains over a long Cases tuple are recursion-heavy. Split or cap depth in performance-sensitive files (see Core / Composing tests on performance).