Sooner or later you write a function or data structure that needs to work with different types in the same way: an API response wrapper, a cache, a "return the first element of an array" function. Generics are the way to write such code once without losing type checking. Let's look at why they're needed and how to use them.
Why generics are needed
Imagine a function that returns the first element of an array. Without generics there are two bad options.
The first — write a separate function for each type. That's duplication: firstNumber, firstString, firstUser — and so on forever.
The second — use any to accept anything:
function first(arr: any[]): any {
return arr[0];
}
const n = first([1, 2, 3]); // type of n is any, checks are gone
n.toUpperCase(); // compiler is silent, but there will be a runtime error
Here we switched off type checking: n has type any, and TypeScript will no longer tell you that a number has no toUpperCase method. Generics solve exactly this problem — they let you link the type of the result to the type of the input.
Short rule: a generic is a "type parameter" that is substituted at the call site, just like a regular argument, but for types.
Generic functions
Let's rewrite first with a generic. In angle brackets after the name we declare a type parameter — by convention it is called T (for type):
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
The type T is not set in advance — it is inferred from the argument. Pass an array of numbers — T becomes number, and the result is also number. Pass strings — T becomes string. One piece of code, full type checking.
Most of the time T is inferred automatically, but you can also specify it explicitly:
const x = first<string>(["a", "b"]); // x: string | undefined
There can be multiple type parameters. A classic example — a function that merges two objects:
function merge<A, B>(a: A, b: B): A & B {
return { ...a, ...b };
}
const result = merge({ name: "Ann" }, { age: 30 });
// result: { name: string } & { age: number }
result.name; // ok
result.age; // ok
Generic types and interfaces
Generics work not only in functions. Often you need a generic type — a structure whose shape is the same but whose content varies. The typical case is an API response wrapper:
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
// substitute the concrete type when using
type User = { id: number; name: string };
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Ann" },
status: 200,
error: null,
};
userResponse.data.name; // type is known: string
The same can be written with type:
type Box<T> = {
value: T;
};
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
The type parameter can be "passed through" further — for example, a paginated list wrapper reuses ApiResponse:
type Page<T> = {
items: T[];
total: number;
};
type UsersPage = ApiResponse<Page<User>>;
Constraints with extends
Sometimes a generic should work not with any type, but only with one that has a required property. For example, a "return the length" function only makes sense for values that have a length. If you leave T unconstrained, the compiler will rightly forbid accessing .length — it doesn't know it's there.
The solution is a constraint via extends. It says: "T can be anything, but it must conform to this shape":
function logLength<T extends { length: number }>(value: T): T {
console.log(value.length); // .length is guaranteed to exist now
return value;
}
logLength("hello"); // ok: strings have length
logLength([1, 2, 3]); // ok: arrays have length
logLength(42); // error: numbers have no length
A common pattern — constraining one type parameter by the keys of another. The keyof T operator produces a union of T's property names, and we require the key to belong to the object:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ann" };
getProperty(user, "name"); // return type: string
getProperty(user, "id"); // return type: number
getProperty(user, "email"); // error: "email" is not among the keys
T[K] here is "the type of property K on object T". The compiler knows exactly that getProperty(user, "id") returns number, not something arbitrary. This is reusable type-safe code: one function, correct for any object and any of its fields.
Utility types — ready-made generics
TypeScript ships with built-in utility types — generic types that build a new type from an existing one. They are written with the same generics we covered above, and they save a lot of manual work. A few of the most common:
type User = {
id: number;
name: string;
email: string;
};
// Partial<T> — all fields become optional.
// Useful for update functions that change only some fields.
function updateUser(id: number, patch: Partial<User>) {
// patch can be { name: "Bob" } or { email: "...", name: "..." }
}
// Pick<T, K> — select only the listed fields.
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
// Record<K, V> — an object with keys K and values V.
// For example, a dictionary "role → list of permissions".
type Permissions = Record<string, string[]>;
const perms: Permissions = {
admin: ["read", "write"],
guest: ["read"],
};
There are others too — Omit (exclude fields), Readonly (make all fields read-only), Required (make all fields required). The key insight is that these are not magic — they are ordinary generics, and you can write similar ones yourself when needed.
In short
- A generic is a type parameter substituted at the call site that links the types of inputs and outputs; an alternative to code duplication and the dangerous
any. - In functions,
Tis usually inferred from arguments automatically, but can also be specified explicitly. - Not only functions but also
type/interfacecan be generic — a shape with a variable filling (response wrappers, containers, lists). - The constraint
T extends ...restricts the allowed types and gives access to guaranteed properties;keyofandT[K]make field access type-safe. - Utility types (
Partial,Pick,Record,Omit,Readonly) are ready-made generics from the standard library for transforming types.
What to read next
- TypeScript basic types — the foundation that generics build on.
- Type narrowing and type guards — how TypeScript refines types inside branches.
- Classes and decorators — generics also work in classes, for example in generic repositories.