TypeScript Generics Guide
Generics are how you write code that preserves type information instead of falling back to
any. This guide walks from basic type parameters through
constraints, defaults, conditional types, and infer.
1. Type parameters
A type parameter is a placeholder that gets filled in when the function or type is used. By
convention single-letter names like T,
U, K, but full names
are fine when there are more than one.
function identity<T>(x: T): T { return x; }
identity(42); // T inferred as number
identity("hello"); // T inferred as string
identity<User>(u); // T explicit
2. Constraints
Use extends to narrow what the caller can pass. Inside the
function, the constraint becomes the upper bound for the parameter.
function getLength<T extends { length: number }>(x: T): number {
return x.length;
}
getLength("hi"); // OK
getLength([1, 2, 3]); // OK
getLength({ length: 5 }); // OK
getLength(42); // TS2345 — number has no length
3. Multiple parameters and defaults
You can declare multiple type parameters and give them defaults so callers only have to specify what they need.
interface ApiResponse<T = unknown, E = string> {
data?: T;
error?: E;
}
const r1: ApiResponse<User> = { data: { id: 1, name: "Ada" } };
const r2: ApiResponse<User, { code: number; message: string }> =
{ error: { code: 500, message: "boom" } };
4. The keyof trick
keyof T gives you a union of T's property names. Combined
with T[K] you can write strongly-typed property accessors.
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ada" };
get(user, "id"); // number
get(user, "name"); // string
get(user, "x"); // TS2345 — "x" is not a key of user
5. Conditional types
Type-level if:
T extends U ? X : Y. The basis for most fancy utility types.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
6. Distributive conditional types
When you put a naked type parameter on the left of extends,
unions are distributed through the conditional. This is why
Exclude works.
type WithoutNull<T> = T extends null ? never : T;
type X = WithoutNull<string | null | number>;
// ^? string | number (null gets mapped to never and removed)
7. infer
Inside a conditional type, infer X binds a name to a
position inside the type being matched. The base of
ReturnType, Awaited,
and friends.
type ReturnOf<F> = F extends (...args: any) => infer R ? R : never;
function pingPong() { return "pong"; }
type P = ReturnOf<typeof pingPong>; // string
8. Variance gotchas
Generic parameters are covariant by default. If you write a callback that consumes T, you
need to think about strictFunctionTypes. The compiler
checks function parameters contravariantly under that flag — see the
strict mode guide.
9. When NOT to use generics
- You only use the type parameter once — that's just
anywith extra syntax. - The function returns
voidand the parameter is unused in the body. - You can write the constraint as a plain interface that's just as readable.
Convert your JavaScript — then add generics
Generics are easiest to add once the basic types are in place. Convert first, generify second.