Reference · generics

Generics — declare, constrain, infer

A generic is a type parameter: a placeholder filled per use. Read the <…>, find what fills it, predict the result.

Ways to declare a generic

positionformwhen T is chosen
function declfunction f<T>(x: T): Tper call (inferred)
arrowconst f = <T,>(x: T): T => xper call (inferred)
function typetype Fn = <T>(x: T) => Tper call
parameterized aliastype Predicate<T> = (v: T) => booleanwhen you name the alias
interfaceinterface Box<T> { value: T }when you name the interface
classclass Box<T> { … }at new Box<T>() / inferred
Rule of thumb: <T> on the NAME ⇒ caller fixes T when they mention the type. <T> on the FUNCTION ⇒ each call infers its own T. (The <T,> trailing comma on arrows is just JSX disambiguation.)

extends constraints + literal preservation

extends here is a bound (“assignable to”), not inheritance. Asking TS to solve a constrained T makes it pick the narrowest fitting type — the literal.

function a(v: string): string { return v; }
a("x");              // string   ← widened

function b<T extends string>(v: T): T { return v; }
b("x");              // "x"      ← literal preserved
signaturepassing ["A","B"] giveswhy
<T extends string>(s: T[])("A" | "B")[]element constrained ⇒ literals kept
<T extends string[]>(s: T)string[]array constrained ⇒ elements widen

as const — the caller-side alternative

makeStatus1(["A", "B"]);            // string[]
makeStatus1(["A", "B"] as const);   // readonly ["A", "B"]  ← literals + tuple kept
Two routes to keep literals: constrain the element type (author-side), or as const the value (caller-side).

How inference solves T

Inference = constraint-solving. Each occurrence of T is an equation; the arguments are the knowns. With an object implementing an interface, methods are read in declaration order and the first one anchors T.

interface Store<T> {
  list(): T[];                  // ← FIRST = primary constraint
  get(id: string): T;            // ← secondary
  save(id: string, item: T): void;
}
// productStore.list(): Homunculus[]  ⇒  T = Homunculus (drives the whole solve)

Inference priority (return / explicit-param > passive property)

ranksource of Texample
highestexplicit annotationinput: (x: string) => {}
mediummost-constraining inferenceoutput: () => 42
lowestflexible / passive positioninput: (x) => {}
// explicit param wins ⇒ T = string; return value must conform:
testConflict({ input: (x: string) => {}, output: () => 42 }); // ❌ number → string
// drop the annotation ⇒ return drives it ⇒ T = number:
testConflict({ input: (x) => {},          output: () => 42 }); // ✅
The error names the side that had to conform, never the side that won. Find the most explicit position first; that’s T.

Rest / tuple params: <TArgs extends any[]>

formTArgs meansinfers from () => …?
<TArgs extends any[]> ...args: TArgsthe whole tupleyes ⇒ []
<TArgs> ...args: TArgs[]one elementno ⇒ unresolvable
const safeFunction =
  <TArgs extends any[], TResult>(func: (...args: TArgs) => Promise<TResult>) =>
  async (...args: TArgs) => { /* re-spreads the captured signature */ };

// () => Promise<number>          ⇒ TArgs = []
// (s: string) => Promise<number> ⇒ TArgs = [string]
The any[] bound is the tell: it makes TArgs a tuple TS can infer and spread. Without it (TArgs[]), TArgs is a single element and a zero-argument function can’t be solved.