Skip to main content

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 any with extra syntax.
  • The function returns void and 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.