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 | B — boolean 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).