Utility Types
Partial, Required, Pick, Omit, Record, and Extract for transforming types without rewriting them. You'll hit this when you need a version of an existing type with some fields optional or removed.
interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: User) {
// Caller must pass ALL fields, even
// if they only want to update one
}
updateUser("1", {
name: "Alice",
email: "alice@example.com",
age: 30,
}); // Just wanted to update name!interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: User) {
// Caller must pass ALL fields, even
// if they only want to update one
}
updateUser("1", {
name: "Alice",
email: "alice@example.com",
age: 30,
}); // Just wanted to update name!interface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: Partial<User>) {
// Caller passes only the fields to update
}
updateUser("1", { name: "Alice" }); // OKinterface User {
name: string;
email: string;
age: number;
}
function updateUser(id: string, data: Partial<User>) {
// Caller passes only the fields to update
}
updateUser("1", { name: "Alice" }); // OKRequiring the full User object for an update forces callers to re-supply every field, which is tedious and error-prone. If a field is added to User later, every update call breaks. Partial solves this cleanly.
Partial<User> makes every property optional, so callers can pass only the fields they want to update. This is the standard pattern for PATCH-style updates. The type still ensures only valid User properties with correct types are provided.
interface User {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Manually duplicated type
interface UserPreview {
id: string;
name: string;
email: string;
}interface User {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Manually duplicated type
interface UserPreview {
id: string;
name: string;
email: string;
}interface User {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Derived from the source of truth
type UserPreview = Pick<User, "id" | "name" | "email">;interface User {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
// Derived from the source of truth
type UserPreview = Pick<User, "id" | "name" | "email">;Manually copying properties into a separate interface creates two sources of truth. If name changes from string to { first: string; last: string }, the hand-written UserPreview is silently out of date. Pick prevents this.
Pick<User, "id" | "name" | "email"> creates a new type with only those three properties. If User changes a field's type, the preview type updates automatically. This keeps your types DRY and avoids drift between related shapes.
interface DbUser {
id: string;
name: string;
email: string;
passwordHash: string;
}
// Manually rebuilding without passwordHash
interface PublicUser {
id: string;
name: string;
email: string;
}interface DbUser {
id: string;
name: string;
email: string;
passwordHash: string;
}
// Manually rebuilding without passwordHash
interface PublicUser {
id: string;
name: string;
email: string;
}interface DbUser {
id: string;
name: string;
email: string;
passwordHash: string;
}
// Automatically excludes passwordHash
type PublicUser = Omit<DbUser, "passwordHash">;interface DbUser {
id: string;
name: string;
email: string;
passwordHash: string;
}
// Automatically excludes passwordHash
type PublicUser = Omit<DbUser, "passwordHash">;Manually listing all safe fields is fragile. When a new field like avatarUrl is added to DbUser, the manual PublicUser does not include it unless you remember to add it. Omit ensures only the explicitly excluded fields are removed.
Omit<DbUser, "passwordHash"> creates a type with every property except passwordHash. If new fields are added to DbUser, they automatically appear in PublicUser. This is safer than Pick when you want to exclude a small number of fields from a large type.
// Using a plain object with index signature
const themes: { [key: string]: string } = {
light: "#ffffff",
dark: "#1a1a1a",
ocean: "#0066cc",
};
// Any string key is accepted
themes.nonexistent; // No error, undefined// Using a plain object with index signature
const themes: { [key: string]: string } = {
light: "#ffffff",
dark: "#1a1a1a",
ocean: "#0066cc",
};
// Any string key is accepted
themes.nonexistent; // No error, undefinedtype Theme = "light" | "dark" | "ocean";
const themes: Record<Theme, string> = {
light: "#ffffff",
dark: "#1a1a1a",
ocean: "#0066cc",
};
// Only valid keys accepted
themes.light; // OK
themes.forest; // Error: not in Themetype Theme = "light" | "dark" | "ocean";
const themes: Record<Theme, string> = {
light: "#ffffff",
dark: "#1a1a1a",
ocean: "#0066cc",
};
// Only valid keys accepted
themes.light; // OK
themes.forest; // Error: not in ThemeAn index signature { [key: string]: string } accepts any string as a key, so TypeScript cannot catch typos or missing entries. It also allows accessing nonexistent keys without errors, returning undefined at runtime.
Record<Theme, string> creates an object type where every key in the Theme union must be present and map to a string. It catches missing keys (if you add a new theme, you must add a color) and rejects invalid keys. Much safer than an index signature.
type User = { name: string } &
{ email: string } &
{ role: "admin" | "user" };
// Hover shows:
// { name: string } & { email: string }
// & { role: "admin" | "user" }
// Hard to read with many intersectionstype User = { name: string } &
{ email: string } &
{ role: "admin" | "user" };
// Hover shows:
// { name: string } & { email: string }
// & { role: "admin" | "user" }
// Hard to read with many intersectionstype Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type User = Prettify<
{ name: string } &
{ email: string } &
{ role: "admin" | "user" }
>;
// Hover shows:
// { name: string; email: string;
// role: "admin" | "user" }type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
type User = Prettify<
{ name: string } &
{ email: string } &
{ role: "admin" | "user" }
>;
// Hover shows:
// { name: string; email: string;
// role: "admin" | "user" }Raw intersections display as a chain of & in IDE tooltips. With two or three intersections this is manageable, but with more it becomes hard to see what properties are available. The Prettify helper solves this with zero runtime cost since it resolves to the same type.
The Prettify helper uses a mapped type to flatten intersections into a single object type. IDE hover tooltips show a clean, readable object instead of a chain of & intersections. This is especially valuable when composing types from multiple sources like mixins, Pick/Omit combinations, or module augmentations.
const ROLES = ["admin", "editor", "viewer"] as const;
// Manually duplicating the values as a union
type Role = "admin" | "editor" | "viewer";
// Must update both when roles change
function hasRole(role: Role) { }const ROLES = ["admin", "editor", "viewer"] as const;
// Manually duplicating the values as a union
type Role = "admin" | "editor" | "viewer";
// Must update both when roles change
function hasRole(role: Role) { }const ROLES = ["admin", "editor", "viewer"] as const;
// Derive the union from the array
type Role = (typeof ROLES)[number];
// "admin" | "editor" | "viewer"
// Auto-updates when ROLES changes
function hasRole(role: Role) { }const ROLES = ["admin", "editor", "viewer"] as const;
// Derive the union from the array
type Role = (typeof ROLES)[number];
// "admin" | "editor" | "viewer"
// Auto-updates when ROLES changes
function hasRole(role: Role) { }Manually writing the union duplicates the values. When you add a new role to the array but forget to update the type, the type and runtime array drift apart silently. The indexed access pattern keeps them in sync with zero maintenance.
Indexing a tuple or readonly array with [number] extracts a union of all element types. Combined with as const and typeof, this derives a string literal union directly from a runtime array. Adding or removing an element in ROLES updates the Role type automatically.
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; offset: number };
// Manually picking event types
type MouseEvent = { type: "click"; x: number; y: number };
type KeyEvent = { type: "keypress"; key: string };type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; offset: number };
// Manually picking event types
type MouseEvent = { type: "click"; x: number; y: number };
type KeyEvent = { type: "keypress"; key: string };type AppEvent =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; offset: number };
// Automatically extract matching members
type MouseEvent = Extract<AppEvent, { type: "click" }>;
// { type: "click"; x: number; y: number }
type NonMouseEvent = Exclude<AppEvent, { type: "click" }>;
// keypress | scroll eventstype AppEvent =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "scroll"; offset: number };
// Automatically extract matching members
type MouseEvent = Extract<AppEvent, { type: "click" }>;
// { type: "click"; x: number; y: number }
type NonMouseEvent = Exclude<AppEvent, { type: "click" }>;
// keypress | scroll eventsManually duplicating union members creates a maintenance risk. If the click event gains a target property, the hand-written copy is out of date. Extract always reflects the current shape of the source union.
Extract filters a union to members assignable to the given shape. Exclude does the opposite, removing matching members. Both stay in sync when the original union changes. This is cleaner and safer than manually copying type definitions.
// Manually typing what the function returns
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
// Hand-written, can drift from implementation
type NewUser = { id: string; name: string; age: number };
type CreateArgs = [string, number];// Manually typing what the function returns
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
// Hand-written, can drift from implementation
type NewUser = { id: string; name: string; age: number };
type CreateArgs = [string, number];function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
// Derived from the function itself
type NewUser = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }
type CreateArgs = Parameters<typeof createUser>;
// [name: string, age: number]function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
// Derived from the function itself
type NewUser = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }
type CreateArgs = Parameters<typeof createUser>;
// [name: string, age: number]Manually writing the return type and parameter types creates a second source of truth. If createUser starts returning an email field, the hand-written NewUser type is silently wrong. ReturnType and Parameters eliminate this class of bugs.
ReturnType extracts the return type and Parameters extracts the parameter tuple from a function type. Both stay in sync with the implementation automatically. This is especially useful when you do not control the function's source or want to avoid exporting an extra type.