Generics and Utility Types
Write reusable typed functions and transform types with Partial, Pick and more.
What you will learn
- Write a generic function
- Use utility types
- Constrain generics
Sometimes the same logic works for many types: returning the first item of any array, wrapping any value in a response. Generics let you write that code once while keeping full type safety, rather than falling back to any.
function first<T>(items: T[]): T | undefined {
return items[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefinedT is a type parameter, a placeholder filled in from the arguments. The compiler remembers that numbers went in, so numbers come out.
interface ApiResponse<T> {
data: T;
error: string | null;
}
interface Product { id: number; title: string; }
const res: ApiResponse<Product[]> = {
data: [{ id: 1, title: "Pen" }],
error: null,
};Constraints
Use extends to require that a type parameter has certain properties.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("apple", "fig"); // works for strings
longest([1, 2], [1, 2, 3]); // and arrays
// longest(1, 2); // Error: number has no lengthfunction getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProp({ id: 1, name: "Ada" }, "name"); // string
// getProp({ id: 1 }, "email"); // Error: not a keyUtility types
TypeScript ships helpers that build new types from existing ones.
interface User { id: number; name: string; email: string; }
type Draft = Partial<User>; // every field optional
type Required_ = Required<User>; // every field required
type Preview = Pick<User, "id" | "name">; // only these keys
type NoEmail = Omit<User, "email">; // all but this key
type Roles = Record<string, "admin" | "user">; // map of string keys
type Frozen = Readonly<User>;
function updateUser(id: number, changes: Partial<User>) { /* ... */ }const config = { host: "localhost", port: 5432 };
type Config = typeof config; // { host: string; port: number }
async function load() { return { id: 1, name: "Ada" }; }
type Loaded = Awaited<ReturnType<typeof load>>; // { id: number; name: string }Generics are powerful but can become cryptic. If a type takes minutes to understand, consider simplifying it, or splitting it into named pieces.
Try it yourself
Write a generic groupBy<T>(items: T[], key: keyof T) returning a record from key value to arrays of items.
Show solution
function groupBy<T>(items: T[], key: keyof T): Record<string, T[]> {
const out: Record<string, T[]> = {};
for (const item of items) {
const k = String(item[key]);
(out[k] ??= []).push(item);
}
return out;
}
groupBy([{ t: "a", n: 1 }, { t: "a", n: 2 }, { t: "b", n: 3 }], "t");