TypeScript Utility Types: Complete Guide to Partial, Pick, Omit and Record

A complete walkthrough of TypeScript's built-in utility types, showing how to derive new types from existing ones instead of maintaining near-duplicate interfaces that silently drift apart.

Most type duplication in a growing codebase follows the same pattern: a User interface, then a UserUpdate with every field optional, then a UserPublic without the password, then a UserForm with everything as strings. Written by hand, these four definitions must be kept in sync manually — and the day someone adds a field to one and forgets the others is the day a bug ships.

Utility types solve this by deriving each variant from a single source of truth. Add a field to User and every derived type updates automatically.

Changing Modifiers Across Every Property

These three utilities rewrite the optionality or mutability of every property in a type. They are the most frequently used utilities in application code, particularly around update operations.

TypeScript
Deriving an update payload type with Partial.
interface User {
  id: number;
  name: string;
  email: string;
  bio?: string;
}

// Partial<T> — every property becomes optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; bio?: string }

function updateUser(id: number, changes: Partial<Omit<User, "id">>): void {
  // Callers may send any subset of the editable fields
}

updateUser(1, { name: "Ada" });
updateUser(1, { email: "ada@example.com", bio: "Engineer" });

// Required<T> — removes optionality, the inverse of Partial
type CompleteUser = Required<User>;
// bio is now required: string

// Readonly<T> — every property becomes read-only
type FrozenUser = Readonly<User>;
const u: FrozenUser = { id: 1, name: "Ada", email: "a@b.c" };
// Error: Cannot assign to 'name' because it is a read-only property.
// u.name = "Grace";
Note: All three are shallow. Readonly<T> freezes top-level properties only — a nested object or array inside remains mutable. For deep immutability you need a recursive mapped type.
TypeScript
A recursive DeepReadonly for nested structures.
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  server: { host: string; port: number };
  flags: string[];
}

const cfg: DeepReadonly<Config> = {
  server: { host: "localhost", port: 3000 },
  flags: ["beta"],
};

// Error: Cannot assign to 'port' because it is a read-only property.
// cfg.server.port = 8080;

Selecting and Removing Properties by Key

Pick builds a type from a chosen subset of keys; Omit builds one from everything except the named keys. The choice between them is about which list is more stable over time.

TypeScript
Pick for narrow projections, Omit for exclusions.
interface User {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

// Pick — name exactly what you want
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit — name only what you want gone
type PublicUser = Omit<User, "passwordHash">;
// { id, name, email, createdAt }

// Omit is the safer default for security-sensitive projections:
// a newly added secret field is NOT automatically exposed only if
// you remember to omit it — so prefer Pick for API responses.
type ApiUser = Pick<User, "id" | "name" | "email">;

There is a meaningful security distinction here. If you use Omit<User, "passwordHash"> for an API response and later add a twoFactorSecret field to User, that secret is immediately included in the response type. Pick fails closed: new fields are excluded until explicitly added.

UtilityBehaviourUse when
Pick<T, K>Keeps only listed keysAllow-list — API responses, DTOs, projections
Omit<T, K>Removes listed keysDeny-list — stripping an id before insert
Partial<T>All properties optionalPATCH payloads, option bags, builders
Required<T>All properties mandatoryPost-validation, resolved config
Note: Omit does not check that the keys you remove actually exist on T — Omit<User, "passwrdHash"> compiles and silently removes nothing. Pick does enforce that keys exist, which is another reason to prefer it.

Building Object Types from a Key Union

Record<K, V> constructs an object type with keys K and values V. Combined with a union of string literals it produces exhaustive lookup tables the compiler will police.

TypeScript
Record for exhaustive maps keyed by a literal union.
type Role = "admin" | "editor" | "viewer";

interface Permissions {
  read: boolean;
  write: boolean;
  delete: boolean;
}

// Every Role must have an entry — omitting one is a compile error
const rolePermissions: Record<Role, Permissions> = {
  admin:  { read: true,  write: true,  delete: true  },
  editor: { read: true,  write: true,  delete: false },
  viewer: { read: true,  write: false, delete: false },
};

// Add "auditor" to Role and this object immediately errors:
// Property 'auditor' is missing in type ...

// For genuinely open-ended keys, use an index signature instead
const cache: Record<string, string> = {};
cache["any-key"] = "value";

The exhaustiveness is the point. Using Record over a closed union turns "we forgot to handle the new role" from a runtime surprise into a build failure.

Operating on Unions Rather Than Objects

These utilities filter union members instead of object properties. They are built on distributive conditional types, which apply a check to each member of a union independently.

TypeScript
Filtering union members.
type Status = "idle" | "loading" | "success" | "error";

// Exclude<T, U> — remove members assignable to U
type Settled = Exclude<Status, "idle" | "loading">;
// "success" | "error"

// Extract<T, U> — keep only members assignable to U
type Pending = Extract<Status, "idle" | "loading">;
// "idle" | "loading"

// NonNullable<T> — strip null and undefined
type MaybeName = string | null | undefined;
type Name = NonNullable<MaybeName>;  // string

// Extract is especially useful over discriminated unions
type Action =
  | { type: "add"; payload: number }
  | { type: "remove"; id: string }
  | { type: "reset" };

type AddAction = Extract<Action, { type: "add" }>;
// { type: "add"; payload: number }

That last pattern — extracting one variant of a discriminated union by its tag — avoids declaring each action interface separately just so it can be referenced by name elsewhere.

ReturnType, Parameters and Awaited

This group derives types from function signatures, which is how you stay in sync with code whose types you do not control or do not want to restate.

TypeScript
Deriving types from existing functions.
function createSession(userId: number, ttlSeconds: number) {
  return {
    token: crypto.randomUUID(),
    userId,
    expiresAt: new Date(Date.now() + ttlSeconds * 1000),
  };
}

// ReturnType — the shape the function produces
type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }

// Parameters — the argument list as a tuple
type CreateArgs = Parameters<typeof createSession>;
// [userId: number, ttlSeconds: number]

// Awaited — unwrap a Promise, recursively
async function loadUser() {
  return { id: 1, name: "Ada" };
}
type LoadedUser = Awaited<ReturnType<typeof loadUser>>;
// { id: number; name: string }

// Useful for wrapping third-party functions without restating types
function logged<F extends (...args: any[]) => any>(fn: F) {
  return (...args: Parameters<F>): ReturnType<F> => {
    console.log(`calling ${fn.name}`, args);
    return fn(...args);
  };
}

Awaited<T> unwraps nested promises, so Awaited<Promise<Promise<string>>> is string — it matches the runtime behaviour of await rather than peeling a single layer.

• Derive variant types from one source interface rather than writing them by hand.
• Prefer Pick over Omit for anything user-facing, so new fields are excluded by default.
• Use Record over a closed literal union to get compile-time exhaustiveness.
• Reach for ReturnType and Parameters to avoid restating types you do not own.

Summary

TypeScript's utility types turn type maintenance from a manual synchronisation problem into a derivation problem. One interface becomes the source of truth, and every variant follows from it automatically.

Use Partial, Required and Readonly to adjust modifiers; Pick and Omit to project properties; Record to build exhaustive key-driven maps; Exclude, Extract and NonNullable to filter unions; and ReturnType, Parameters and Awaited to derive from functions.

The practical payoff is that adding a field to a core model surfaces every place that needs attention at build time, instead of leaving stale duplicate interfaces to be discovered in production.