webdev.complete
🧠 Advanced TypeScript
🛡️TypeScript
Lesson 44 of 117
25 min

Utility Types

Partial, Pick, Omit, Record, ReturnType, Awaited.

Utility types are TypeScript's standard library for transforming one type into another. You almost never want to copy a type and tweak it by hand. You want to derive the new one from the old, so when the source changes, the derived type stays in sync. Master a dozen of these and your codebase gets smaller, safer, and dramatically easier to refactor.

The source type we'll transform

Imagine a typical database entity. Every utility below will derive a new type from this one.

ts
type User = {
  id: string;
  email: string;
  name: string;
  age: number;
  passwordHash: string;
  createdAt: Date;
};

Partial<T>: every key becomes optional

The classic use case: a function that updatesa user. You don't want to require the caller to pass every field again. Only the ones changing.

ts
type UserUpdate = Partial<User>;
// { id?: string; email?: string; name?: string; age?: number; ... }

function updateUser(id: string, changes: Partial<User>): User {
  // merge changes into the stored user
  return { ...getStored(id), ...changes };
}

updateUser("u_1", { name: "Ada Lovelace" });             // OK
updateUser("u_1", { name: "Ada", age: 36 });             // OK
updateUser("u_1", { name: "Ada", typo: true });          // Error: unknown field

Required<T>: every key becomes required

Useful when a type has lots of optionals and you want a "complete" variant.

ts
type Config = { host?: string; port?: number; tls?: boolean };
type FullConfig = Required<Config>;
// { host: string; port: number; tls: boolean }

function start(c: FullConfig) { /* every field guaranteed */ }

Readonly<T>: every key becomes readonly

ts
type Frozen = Readonly<User>;

const u: Frozen = { /* ... */ } as Frozen;
// u.name = "Grace"; // Error: Cannot assign to 'name' because it is read-only
Shallow, not deep
Readonly<T> only freezes the top level. If a property is itself an object, you can still mutate that nested object. For deep readonly, you build it yourself with mapped types (see the next lesson).

Pick<T, K>: keep these keys, drop the rest

Perfect for shipping a public summary of an internal type. The canonical example: a "PublicUser" that excludes the password hash.

ts
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: string; name: string; email: string }

function getPublicProfile(u: User): PublicUser {
  return { id: u.id, name: u.name, email: u.email };
}

Omit<T, K>: drop these keys, keep the rest

The inverse of Pick. Often more readable when you want to remove a few sensitive fields from a wide entity.

ts
type SafeUser = Omit<User, "passwordHash">;
// every field except passwordHash

type CreateUserInput = Omit<User, "id" | "createdAt">;
// the shape the API accepts, since the server generates id and createdAt
Pick or Omit?
Use Pick when the keep list is short and the type is wide. Use Omit when the drop list is short. Both produce identical types when applied right. Pick whichever reads more clearly at the call site.

Record<K, V>: a dictionary type

Build an object type where keys are constrained and every value has the same type. The most common pattern for lookup tables and configuration maps.

ts
// A status map keyed by status code
type StatusMessages = Record<200 | 404 | 500, string>;
const msgs: StatusMessages = {
  200: "OK",
  404: "Not Found",
  500: "Server Error",
};

// A feature flag table
type FeatureFlags = Record<string, boolean>;

// A typed cache
const cache: Record<string, User> = {};
cache["u_1"] = { id: "u_1", /* ... */ } as User;

Exclude and Extract: set operations on unions

These two work on union types, not object types. Think of them as set subtraction and set intersection.

ts
type Status = "pending" | "active" | "done" | "error";

// Remove members
type ActiveStates = Exclude<Status, "error">;
// "pending" | "active" | "done"

// Keep only matching members
type TerminalStates = Extract<Status, "done" | "error">;
// "done" | "error"

// Real-world: get all the function-typed values of a type
type Methods<T> = {
  [K in keyof T]: T[K] extends Function ? K : never
}[keyof T];

NonNullable<T>: strip null and undefined

ts
type MaybeName = string | null | undefined;
type DefiniteName = NonNullable<MaybeName>;
// string

// Common with optional chaining
function take(user: User | null) {
  if (!user) return;
  // here user is NonNullable<User | null> = User
}

ReturnType and Parameters: extract from a function type

Two of the most useful utilities in real codebases. They let you derive types from a function's signature, so you never duplicate the function's contract by hand.

ts
function fetchUser(id: string, opts?: { fresh: boolean }) {
  return { id, name: "Ada", age: 36 };
}

// What does this function return?
type User = ReturnType<typeof fetchUser>;
// { id: string; name: string; age: number }

// What are its parameters?
type FetchArgs = Parameters<typeof fetchUser>;
// [id: string, opts?: { fresh: boolean }]

// Spread the args back into another function
function logAndFetch(...args: FetchArgs) {
  console.log("Fetching with", args);
  return fetchUser(...args);
}
The typeof trick
typeof fetchUserin a type position gets you the function's type. Combine with ReturnType or Parameters and you never duplicate a signature. If the source function changes, every derived type updates automatically.

Awaited<T>: unwrap a Promise

Until TS 4.5 there was no built-in way to express "the thing inside a Promise." Awaited solves it, and it unwraps nested promises too.

ts
async function loadUser() {
  return { id: "u_1", name: "Ada" };
}

type UserResult = Awaited<ReturnType<typeof loadUser>>;
// { id: string; name: string }

// Unwraps recursively
type X = Awaited<Promise<Promise<string>>>;
// string

A realistic API stack

Here's how this all comes together in a real codebase. Notice how every derived type is just a transform of the source. Change User, and the whole chain stays consistent.

users-api.ts
// The canonical entity stored in the database
type User = {
  id: string;
  email: string;
  name: string;
  passwordHash: string;
  createdAt: Date;
};

// What the API accepts when creating
type CreateUserInput = Omit<User, "id" | "createdAt" | "passwordHash"> & {
  password: string;
};

// What the API accepts when updating, where every field is optional
type UpdateUserInput = Partial<Omit<User, "id" | "createdAt">>;

// What we send back to the client, never the hash
type UserResponse = Omit<User, "passwordHash">;

// A map of users keyed by id
type UserById = Record<string, UserResponse>;

// The shape of the list endpoint
async function listUsers(): Promise<UserResponse[]> {
  // ...
  return [];
}

// Derive the unwrapped element type from the function itself
type ListItem = Awaited<ReturnType<typeof listUsers>>[number];
// = UserResponse, even if listUsers changes

InstanceType and ConstructorParameters

Less common, but lifesavers when working with classes you do not control:

ts
class HttpClient {
  constructor(public baseUrl: string, public timeoutMs = 5000) {}
}

type Client = InstanceType<typeof HttpClient>;
// HttpClient

type CtorArgs = ConstructorParameters<typeof HttpClient>;
// [baseUrl: string, timeoutMs?: number]
Quiz1 / 4

Which utility type derives an 'update' DTO where every field is optional?

Recap

  • Partial, Required, Readonly flip modifiers on every key.
  • Pick keeps listed keys. Omit drops them. Inverses.
  • Record<K, V> builds a dictionary type.
  • Exclude and Extract are set operations on unions.
  • NonNullable strips null | undefined.
  • ReturnType and Parametersderive types from a function's signature. Awaited unwraps a Promise.
  • The pattern is always the same: derive new types from a single source of truth.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.