Reference · generics
A generic is a type parameter: a placeholder filled per use. Read the
<…>, find what fills it, predict the result.
| position | form | when T is chosen |
|---|---|---|
| function decl | function f<T>(x: T): T | per call (inferred) |
| arrow | const f = <T,>(x: T): T => x | per call (inferred) |
| function type | type Fn = <T>(x: T) => T | per call |
| parameterized alias | type Predicate<T> = (v: T) => boolean | when you name the alias |
| interface | interface Box<T> { value: T } | when you name the interface |
| class | class Box<T> { … } | at new Box<T>() / inferred |
Rule of thumb:<T>on the NAME ⇒ caller fixesTwhen they mention the type.<T>on the FUNCTION ⇒ each call infers its ownT. (The<T,>trailing comma on arrows is just JSX disambiguation.)
extends constraints + literal preservationextends 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
| signature | passing ["A","B"] gives | why |
|---|---|---|
| <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 alternativemakeStatus1(["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).
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)
| rank | source of T | example |
|---|---|---|
| highest | explicit annotation | input: (x: string) => {} |
| medium | most-constraining inference | output: () => 42 |
| lowest | flexible / passive position | input: (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.
<TArgs extends any[]>| form | TArgs means | infers from () => …? |
|---|---|---|
| <TArgs extends any[]> ...args: TArgs | the whole tuple | yes ⇒ [] |
| <TArgs> ...args: TArgs[] | one element | no ⇒ 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]
Theany[]bound is the tell: it makesTArgsa tuple TS can infer and spread. Without it (TArgs[]),TArgsis a single element and a zero-argument function can’t be solved.