Generics
|
This section documents the current TypeScript release line as published at the official TypeScript documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since TypeScript iterates quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A generic is a type with holes in it. You name the holes as type parameters and TypeScript fills them in — usually by inference at the call site — so one definition serves every element type. The official Generics handbook chapter is the companion to this page; what follows is the working summary.
Where type parameters are declared
A type parameter list <T> can be attached to a function, an interface, a class, or a type alias. It is
in scope for the rest of that declaration.
// Generic function -- T is bound when the function is called.
function first<T>(items: readonly T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // T inferred as number -> number | undefined
const s = first(["a", "b"]); // T inferred as string -> string | undefined
// Generic interface -- T is bound when you write out the type.
interface Box<T> {
value: T;
}
const bn: Box<number> = { value: 1 };
// Generic type alias.
type Pair<A, B> = { left: A; right: B };
const p: Pair<string, number> = { left: "id", right: 42 };
// Generic class -- T is bound at construction.
class Stack<T> {
#items: T[] = [];
push(item: T): void { this.#items.push(item); }
pop(): T | undefined { return this.#items.pop(); }
}
const st = new Stack<string>(); // or: new Stack() and let T be inferred from first push
For a generic function, the type parameter is bound at call time: TypeScript matches the arguments you
pass against the parameter types and solves for T. For a generic interface, alias, or class, it is
bound when you name the type (Box<number>) or when a constructor call gives enough to infer it. Methods
can carry their own type parameters, separate from the container’s.
Constraints
<T extends Constraint> restricts what may be substituted for T, and inside the body T is known to have
at least the constraint’s members.
interface HasLength {
length: number;
}
function longest<T extends HasLength>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // ok -- arrays have length
longest("ab", "abc"); // ok -- strings have length
// longest(10, 20); // Error: number has no 'length'
The return type is T, not HasLength, so the caller keeps the precise type it passed in.
keyof plus a constrained parameter
Pairing keyof with a second, constrained type parameter is the canonical typed property accessor: K
ranges only over the keys of T, and the result is the exact property type.
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada", active: true };
const id = getProp(user, "id"); // number
const name = getProp(user, "name"); // string
// getProp(user, "email"); // Error: "email" is not a key of user
keyof, indexed access T[K], and mapped types are covered in
Type Manipulation.
Defaults and multiple type parameters
A type parameter may have a default, and a later parameter’s default or constraint can refer to an earlier one.
interface ApiResult<TData = unknown, TError = Error> {
data?: TData;
error?: TError;
}
const r1: ApiResult = {}; // TData = unknown, TError = Error
const r2: ApiResult<string> = { data: "ok" }; // TError still defaults to Error
// A default that depends on an earlier parameter:
type Dictionary<V, K extends string | number = string> = Record<K, V>;
const scores: Dictionary<number> = { alice: 10 }; // K defaults to string
Defaults apply only where TypeScript cannot otherwise infer the argument; an inferred type always wins over the default.
Generics as functions between types
Read a generic as a function whose inputs and output are types: Array<T> maps the type T to the type
"array of T`"; `Pick<T, K> maps a type and a key union to a narrowed object type. This is why the utility
types in Utility Types are all generic — each is a small
type-level function you apply with <…>.
type Nullable<T> = T | null; // T -> T | null
type Unwrap<T> = T extends Promise<infer U> ? U : T; // Promise<X> -> X, else T
type A = Nullable<number>; // number | null
type B = Unwrap<Promise<string>>; // string
type C = Unwrap<boolean>; // boolean
The golden rule: a type parameter must relate two positions
A type parameter earns its place only if it appears at least twice — linking an input to another input,
or an input to the output. A parameter used once is not doing any work and should be replaced by the type it
stands for (often unknown or an explicit union).
// Unnecessary: P appears once. This is just (x: unknown) => void with extra noise,
// and the annotation gives callers a false sense of type safety.
function logOnce<P>(x: P): void {
console.log(x);
}
// Better:
function logPlain(x: unknown): void {
console.log(x);
}
// Legitimate: T ties the argument to the return value.
function identity<T>(x: T): T {
return x;
}
// Legitimate: T ties two arguments together, K ties the key to the result.
function pluck<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
A related smell is a type parameter that only ever appears in the return position with nothing to infer
from — that is an unsafe cast in disguise (function make<T>(): T), and the caller can pick any T.
Naming conventions
Single uppercase letters are the norm: T for a lone parameter, T/U/V for successive ones, K for a
key type, V for a value type, E for an element, R for a return type. When a generic has several
parameters with distinct roles, prefer descriptive PascalCase names with a T prefix (TData,
TError, TContext) so signatures stay readable.
Inference and how to guide it
TypeScript infers type arguments from the values you pass. You can override that by writing the type arguments explicitly, or shape the inference by how a parameter is typed.
function wrap<T>(value: T): { value: T } {
return { value };
}
wrap("hello"); // T inferred as string
wrap<"hello">("hello"); // T pinned to the literal type "hello"
// Guide inference toward a literal-preserving shape with a constraint:
function keys<T extends Record<string, unknown>>(o: T): (keyof T)[] {
return Object.keys(o) as (keyof T)[];
}
keys({ a: 1, b: 2 }); // ("a" | "b")[]
Because plain object and array literals widen ("hello" becomes string), inference often loses the
literal types you wanted.
const type parameters
Marking a type parameter const makes TypeScript infer the most specific (as-if as const) type for
arguments matched to it — tuples stay tuples, string literals stay literals — without the caller writing
as const. Added in TypeScript 5.0; see
the
5.0 release notes.
function route<const T extends readonly string[]>(segments: T): T {
return segments;
}
const r = route(["users", "profile"]);
// ^? const r: readonly ["users", "profile"] -- not string[]
NoInfer<T>
NoInfer<T> (TypeScript 5.4) marks a spot as not an inference source, so T is solved from the other
positions only. Use it to make one argument follow another instead of widening the parameter.
function paint<T extends string>(palette: readonly T[], selected: NoInfer<T>): T {
return selected;
}
paint(["red", "green", "blue"], "green"); // ok
// paint(["red", "green", "blue"], "purple");
// Error: "purple" is not assignable to "red" | "green" | "blue"
// Without NoInfer, T would widen to include "purple" and the mistake would compile.
See NoInfer<T> in the
utility types reference, and Utility Types.
Promise<T>
Promise<T> is the generic that types an eventual value: T is what the promise resolves to. A rejection
carries no type — it is always any/unknown at the catch.
function fetchUser(id: number): Promise<{ id: number; name: string }> {
return fetch(`/api/users/${id}`).then((res) => res.json());
}
fetchUser(1).then((user) => {
user.name; // string -- T flows through .then
});
An async function always returns a Promise; you annotate the inner type and TypeScript wraps it
(async function f(): Promise<number> — the body return`s a `number). Awaiting a Promise<T> yields
T. That mechanism, plus Awaited<T>, error typing, and async iterators, lives on
Async and Iterators.
See also
-
Type Manipulation —
keyof, indexed access, conditional and mapped types that generics are built from. -
Utility Types — the standard library of generic type-level functions.
-
Functions — generic call signatures, overloads, and
thistyping. -
Classes — generic classes and methods.
-
Async and Iterators —
Promise<T>,Awaited<T>, and generic iterator/generator types.