TypeScript Generics
Generics let you write functions and types that work across many types while still preserving full type safety, instead of falling back to any.
Why Generics Exist
Without generics, a reusable function either has to be typed for one specific type, which limits reuse, or typed as any, which throws away all type checking. Generics solve this by letting the caller supply the type as a parameter, so the function stays reusable and fully type-checked at the same time.
A Generic Function
function firstElement<T>(items: T[]): T | undefined {
return items[0];
}
const firstNum = firstElement([10, 20, 30]); // number | undefined
const firstStr = firstElement(["a", "b"]); // string | undefined
console.log(firstNum, firstStr);Generic Interfaces and Type Aliases
Generics aren't limited to functions. Interfaces and type aliases can take type parameters too, which is how you build reusable shapes like a paginated response or a key-value cache that adapts to whatever content type you plug in.
A Generic Interface
interface ApiResponse<T> {
data: T;
success: boolean;
}
function wrapResponse<T>(data: T): ApiResponse<T> {
return { data, success: true };
}
const res = wrapResponse({ id: 1, name: "Aiko" });
console.log(res.data.name, res.success);Constraining Generics with extends
Sometimes a generic function needs to assume the type has certain properties, such as a length or an id. The extends keyword constrains a type parameter to only accept types matching a given shape, giving you access to those members inside the function.
- T extends U restricts the type parameter to types compatible with U.
- Constraints let you safely access properties inside the generic function body.
- You can use multiple type parameters, each with its own independent constraint.
- Default type parameters (T = string) let callers omit the type argument entirely.
Constrained Generic
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find((item) => item.id === id);
}
const users = [{ id: 1, name: "Ana" }, { id: 2, name: "Leo" }];
console.log(findById(users, 2));A Realistic Use Case: A Generic Cache
Generics shine in shared utilities like a small in-memory cache, which needs to store any value type while still returning the correct type back out when a value is read.
Generic Cache Class
class Cache<T> {
private store = new Map<string, T>();
set(key: string, value: T): void {
this.store.set(key, value);
}
get(key: string): T | undefined {
return this.store.get(key);
}
}
const userCache = new Cache<{ name: string }>();
userCache.set("u1", { name: "Mira" });
console.log(userCache.get("u1")?.name);Exercise: TypeScript Generics
Does a generic type parameter have to be named `T`?