Overview
A generic turns one function into a family of typed functions; the cost is a type parameter, and the win is that callers keep their type information. Add one only when a real shape varies between calls. Read typescript first.
Add a type parameter only when the output depends on the input
T earns its place when it appears in at least two positions, such as a parameter and the return type. If every call site must pass <Foo> explicitly, inference is not working and the generic is a cast in disguise.
function first<T>(items: readonly T[]): T | undefined {
return items[0]
}
const n = first([1, 2, 3]) // number | undefinedConstrain with extends
An unconstrained T can be anything. Constrain it to what the body uses; K extends keyof T is the most useful constraint and powers Pick and typed accessors.
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}Steer inference instead of annotating call sites
TypeScript infers type parameters from arguments, so annotate only when it cannot.
NoInfer<T>(5.4) blocks a parameter from contributing to inference, socreateStore<T>(initial: T, fallback: NoInfer<T>)infersTfrominitialalone and errors on a mismatchedfallback.<const T>(5.0) infers literal types for object and array arguments without the caller writingas const.- Give a type parameter a default (
<TState = Record<string, unknown>>) when most callers want one shape.
Return Result types at API boundaries
A discriminated Result<T, E> puts failure in the signature, so callers cannot ignore it the way they can ignore a thrown exception. Narrowing on ok is automatic (typescript-narrowing).
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
async function loadInvoice(id: string): Promise<Result<Invoice, "not-found" | "network">> {
const res = await fetch(`/api/invoices/${id}`)
if (res.status === 404) return { ok: false, error: "not-found" }
if (!res.ok) return { ok: false, error: "network" }
return { ok: true, value: Invoice.parse(await res.json()) }
}Use infer when no utility type exists
infer pulls a piece out of a type inside a conditional. ReturnType, Parameters, and Awaited are all built this way, so check typescript-utility-types first.
type ArrayItem<T> = T extends readonly (infer U)[] ? U : neverKeep application generics shallow
Heavy generic types make compiler errors unreadable. Name the intermediate type when an expression needs more than two lines to understand, split a function with four type parameters, and refactor a chain of nested conditional types. Library types can earn complexity; application types rarely do. Abstract over many resources with the same lifecycle only once you have three of them, per general-principles. For compile-time cost of deep type graphs, see typescript-types-vs-interfaces.