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.

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 timeany 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 | undefinedRead <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 lengthextends 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 userkeyof 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; // stringHere you do write the type at the call, because there is no argument to infer it from.
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.
| Type | Meaning |
|---|---|
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?
Should I name parameters T, U and V?
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.