Cookbook
Copy-pasteable patterns for common type-level work.
A grab bag of patterns that keep showing up. Each one references the modules involved.
Pin a literal in a generic parameter
import type { Narrow } from '@gentleduck/ttest/literal'
function defineRoles<T extends ReadonlyArray<string> & Narrow<T>>(roles: T): T {
return roles
}
const r = defineRoles(['admin', 'editor']) // type: readonly ['admin', 'editor']
Without Narrow, TypeScript widens to readonly string[].
Exclude any from a generic parameter
import type { NotAny } from '@gentleduck/ttest/any'
function safe<T>(value: T & (NotAny<T> extends true ? unknown : never)): T {
return value
}
safe(1) // ok
safe<any>(1 as any) // Error.
Strip a brand for serialization
import type { Brand, Unbrand } from '@gentleduck/ttest/brand'
type UserId = Brand<string, 'UserId'>
function toJSON<T>(value: T): Unbrand<T> {
return value as Unbrand<T>
}
See Branded types for the full pattern.
Pattern-match a discriminated union
import type { Matchers } from '@gentleduck/ttest/discriminated'
type Shape =
| { kind: 'circle'; r: number }
| { kind: 'square'; side: number }
function dispatch<R>(shape: Shape, handlers: Matchers<Shape, 'kind', R>): R {
return handlers[shape.kind](shape as any)
}
dispatch({ kind: 'circle', r: 1 }, {
circle: (c) => c.r * c.r * Math.PI,
square: (s) => s.side * s.side,
})
See Discriminated unions.
Type-safe Object.entries
import type { Entries } from '@gentleduck/ttest/object'
function entries<T extends object>(o: T): Entries<T>[] {
return Object.entries(o) as Entries<T>[]
}
Validate a route shape at the type level
import type { PathParams, BuildPath } from '@gentleduck/ttest/router'
function link<P extends string, Params extends PathParams<P>>(pattern: P, params: Params): BuildPath<P, Params> {
// runtime substitution omitted
return undefined as any
}
const href = link('/users/:id', { id: 42 }) // type: '/users/42'
Catch JSON-leaks at the boundary
import type { JSONValue, Jsonify } from '@gentleduck/ttest/json'
async function persist<T>(value: Jsonify<T>): Promise<void> {
// value is guaranteed JSON-serializable at the type level
}
persist({ id: 1, name: 'Ada' }) // ok
persist({ created: new Date() }) // Date is stripped → never → Error
persist({ run: () => {} }) // function stripped → never → Error
Cap recursion in deep utilities
import type { Paths } from '@gentleduck/ttest/object'
type Config = {
// ...deep tree...
}
type Top = Paths<Config, 2> // cheap: only first two levels
type Full = Paths<Config> // default cap 6 — usually fine, but slow on very wide trees
Build a typed event emitter
import type { Emitter, EventMap } from '@gentleduck/ttest/emitter'
type AppEvents = {
'user.login': { id: string }
'user.logout': void
}
declare const bus: Emitter<AppEvents>
const off = bus.on('user.login', ({ id }) => console.log(id))
bus.emit('user.logout') // ok, no payload
bus.emit('user.login', { id: '1' })
bus.emit('user.login', { wrong: 1 }) // Error.