TypeScript generics, from confusing to useful

A generic is a type with a hole in it. Build up from one small function to the patterns in real code.

TypeScript generics, from confusing to useful

Generics are where TypeScript stops being "JavaScript with labels" and starts to help you. They are also where many people get lost, because the examples are often abstract. This guide builds up from one small function to the patterns you see in real code.

A generic is a type with a hole in it. The caller fills the hole.
A generic is a type with a hole in it. The caller fills the hole.

The problem

Here is a function that returns the first item of an array.

function first(items: any[]): any {
  return items[0];
}

const n = first([1, 2, 3]); // n is any
n.toUpperCase();            // no error, and a crash at run time

any switched the type checker off. We know that an array of numbers gives back a number, and we did not tell the compiler.

A type parameter

A generic adds a parameter for the type, written in angle brackets.

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([1, 2, 3]);     // number | undefined
const s = first(["a", "b"]);    // string | undefined

Read <T> as "for any type T". The function promises that what comes out has the same type as what went in. You rarely write the type when you call it, because TypeScript infers it from the argument.

Constraints

Sometimes any type is too wide. This function needs a length.

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("hello", "hi");     // fine
longest([1, 2], [1, 2, 3]); // fine
longest(10, 20);            // error: number has no length

extends here means "must have at least this shape". The function still returns the exact type it was given, not the constraint.

Write the function with concrete types first. Make it generic when you copy it for a second type.

Jun Watanabe

keyof: the most useful trick

You want a function that reads a property and keeps its type.

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 7, name: "Ada", admin: true };

const name = get(user, "name");  // string
const admin = get(user, "admin"); // boolean
get(user, "email");               // error: not a key of user

keyof T is the union of the keys of T. T[K] is the type of that property. Together they give you typed access with autocomplete, and a compile error when a key is misspelled.

Generic types, not only functions

Interfaces and type aliases take parameters too. This is how you describe an API response once.

// file: src/api.ts
interface ApiResult<T> {
  data: T;
  fetchedAt: string;
}

interface User { id: number; name: string }

async function getJson<T>(url: string): Promise<ApiResult<T>> {
  const res = await fetch(url);
  return { data: await res.json(), fetchedAt: new Date().toISOString() };
}

const result = await getJson<User[]>("/api/users");
result.data[0].name; // string

Here you do write the type at the call, because there is no argument to infer it from.

⚠️
A generic does not check the data. getJson<User[]> tells the compiler what to expect. It does not verify the response. Validate data from outside your program with a schema library.

Default type parameters

A parameter can have a default, which keeps simple uses short.

interface Paginated<T = unknown> {
  items: T[];
  total: number;
}

The built-in helpers are generics

Once you can read generics, the standard utility types make sense. They are all short.

TypeMeaning
Partial<T>Every property optional
Required<T>Every property required
Pick<T, K>Only the listed keys
Omit<T, K>Everything except the listed keys
Record<K, V>An object with keys K and values V
ReturnType<F>What a function returns
type UserUpdate = Partial<Omit<User, "id">>;
// { name?: string }

You can build a precise type for each use from one source of truth, and a change to User flows through.

A discriminated result

Here is a pattern that combines everything: a result that is either a success or a failure.

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function parsePort(s: string): Result<number, string> {
  const n = Number(s);
  return Number.isInteger(n) && n > 0 && n < 65536
    ? { ok: true, value: n }
    : { ok: false, error: `not a port: ${s}` };
}

const r = parsePort("8080");
if (r.ok) {
  r.value; // number
} else {
  r.error; // string
}

The compiler narrows the type inside each branch. You cannot read value without checking ok first.

When generics go too far

If a signature has four type parameters and three conditional types, stop. Signs that you have gone too far:

  • The error messages are longer than the function
  • You need a comment to explain the type
  • A parameter appears only once in the signature

That last one is a useful test. A type parameter should connect two things: two arguments, or an argument and the return value. If it appears once, a plain type will do.

What is the difference between unknown and any?

Both accept any value. any lets you do anything with it. unknown makes you check the type first. Prefer unknown.

Should I name parameters T, U and V?

For one parameter, T is fine. For several, use names that mean something, such as TItem and TKey.

The habit to build

When you reach for any, ask what you know about the type that the compiler does not. Usually you know that two things are the same type. A type parameter is how you say so.

Great! Check your inbox and click the link to confirm.