On this page

Utility types

12 min read TextCh. 4 — Advanced Types

What are utility types?

TypeScript ships with a set of built-in generic utility types that transform existing types into new ones. Instead of manually rewriting interfaces for every slight variation, you compose transformations on a single source of truth. The result is less code, fewer inconsistencies, and a single place to update when requirements change.

This lesson covers the most important utility types grouped by their purpose.

Transforming all properties

`Partial`

Makes every property of T optional. This is ideal for update operations where the caller provides only the fields that changed:

interface Post {
  id: number;
  title: string;
  body: string;
  published: boolean;
}

function patchPost(id: number, changes: Partial<Post>): void {
  // changes can have any subset of Post properties
  console.log("Applying", changes, "to post", id);
}

patchPost(1, { title: "New title" }); // Only updating the title

`Required`

The opposite of Partial<T>. Every optional property becomes required. Use it when you need to validate that a configuration object is fully populated:

interface AppConfig {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

function startApp(config: Required<AppConfig>): void {
  // Safe to use config.apiUrl without undefined checks
  fetch(config.apiUrl);
}

`Readonly`

Marks every property as readonly, preventing reassignment after creation. It is useful for value objects, configuration records, and constants:

interface Point {
  x: number;
  y: number;
}

const origin: Readonly<Point> = { x: 0, y: 0 };
// origin.x = 1; // Error: cannot assign to 'x' because it is a read-only property

Selecting and removing properties

`Pick`

Creates a new type by keeping only the keys listed in K. Use it to build view models or API responses that expose a subset of a larger model:

interface Article {
  id: number;
  title: string;
  body: string;
  authorId: number;
  createdAt: Date;
  tags: string[];
}

type ArticleCard = Pick<Article, "id" | "title" | "createdAt">;
// Suitable for rendering a list — no body, no internal IDs

`Omit`

Creates a new type by removing the keys listed in K. Use it when it is easier to describe what you want to exclude:

type NewArticleInput = Omit<Article, "id" | "createdAt">;
// User provides everything except the server-generated fields

Building dictionaries

`Record`

Creates an object type with keys of type K and values of type V. It is the typed equivalent of writing { [key: string]: V } but with more precision:

type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500;

const statusMessages: Record<HttpStatus, string> = {
  200: "OK",
  201: "Created",
  400: "Bad Request",
  401: "Unauthorized",
  404: "Not Found",
  500: "Internal Server Error",
};

// TypeScript will error if you forget any key

Filtering union types

`Extract` and `Exclude`

These two are complementary filters on union types:

type Events = "click" | "focus" | "blur" | "mouseenter" | "mouseleave";

type MouseEvents  = Extract<Events, `mouse${string}`>;  // "mouseenter" | "mouseleave"
type NonMouseEvents = Exclude<Events, `mouse${string}`>;  // "click" | "focus" | "blur"

`NonNullable`

Removes null and undefined from a union. Useful after you have already verified that a value is present:

type OptionalId = number | null | undefined;
type DefiniteId = NonNullable<OptionalId>; // number

function processId(id: NonNullable<OptionalId>): string {
  return `ID-${id}`; // No nullability check needed inside
}

Introspecting function types

`ReturnType` and `Parameters`

These utility types let you extract the type information from an existing function without restating it:

function createSession(
  userId: number,
  device: "mobile" | "desktop",
  expiresIn: number
): { token: string; expiresAt: number } {
  const expiresAt = Date.now() + expiresIn * 1000;
  return { token: `tok-${userId}`, expiresAt };
}

type SessionResult = ReturnType<typeof createSession>;
// { token: string; expiresAt: number }

type SessionArgs = Parameters<typeof createSession>;
// [userId: number, device: "mobile" | "desktop", expiresIn: number]

// Use Parameters to build a wrapper with identical signature
function createSessionWithLog(...args: SessionArgs): SessionResult {
  console.log("Creating session for user", args[0]);
  return createSession(...args);
}

`Awaited`

Recursively unwraps Promise<T> until it reaches the underlying value type:

async function loadConfig(): Promise<{ theme: string; lang: string }> {
  return { theme: "dark", lang: "en" };
}

type Config = Awaited<ReturnType<typeof loadConfig>>;
// { theme: string; lang: string }

Practical pattern: full CRUD with utility types

Define one interface and derive all your data-transfer objects from it:

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  categoryId: string;
  createdAt: Date;
  updatedAt: Date;
}

// POST /products
type CreateProductDto = Omit<Product, "id" | "createdAt" | "updatedAt">;

// PATCH /products/:id
type UpdateProductDto = Partial<Omit<Product, "id" | "createdAt" | "updatedAt">>;

// GET /products (list view — no heavy description field)
type ProductSummary = Pick<Product, "id" | "name" | "price" | "categoryId">;

// Cache record keyed by product ID
type ProductCache = Record<string, Readonly<Product>>;

All four types stay automatically in sync whenever the base Product interface changes.


Practice

  1. CRUD DTOs: Define a BlogPost interface with at least 6 properties. Derive CreatePostDto, UpdatePostDto, and PostListItem types using Omit, Partial, and Pick respectively.
  2. Record usage: Create a Record<string, number> that maps country codes to their UTC offset. Write a function that accepts this record and a country code and returns the offset with a default of 0 if the code is not found.
  3. Extract and Exclude: Define a union type of all HTTP method strings ("GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"). Use Extract to produce a type containing only the methods that modify data, and Exclude to produce the read-only methods.
  4. ReturnType and Parameters: Pick any existing function in your codebase (or write a new one). Use ReturnType and Parameters to extract its types and write a thin wrapper that logs the arguments before delegating to the original function.

Next lesson: conditional and mapped types — building your own utility types from scratch.

Combine utility types for DTOs
A common pattern in REST APIs: define one full model interface, then derive DTOs with utility types. `type CreateUserDto = Omit<User, 'id'>` and `type UpdateUserDto = Partial<Omit<User, 'id'>>` give you proper input types without duplication.
Awaited replaces the old UnpackPromise pattern
TypeScript 4.5 introduced `Awaited<T>` which recursively unwraps `Promise<Promise<T>>` all the way to `T`. You no longer need a custom `UnwrapPromise` conditional type in your codebase.
Readonly is shallow
`Readonly<T>` only prevents reassignment of direct properties. Nested objects remain mutable. For deep immutability, use a recursive `DeepReadonly` mapped type or a library like `ts-essentials`.
typescript
interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

// Partial — all properties become optional
type UserDraft = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

// Required — all properties become required (removes optional markers)
type UserComplete = Required<User>;
// { id: number; name: string; email: string; age: number }

// Readonly — all properties become read-only
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: cannot assign to read-only property

// Pick — keep only specified keys
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit — remove specified keys
type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; age?: number }

function updateUser(id: number, patch: Partial<User>): void {
  console.log("Updating user", id, "with", patch);
}

updateUser(1, { name: "Bob" }); // Only name — perfectly valid
typescript
// Record<K, V> — creates an object type with keys K and values V
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;

const perms: Permissions = {
  admin:  ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

// Extract — keep union members assignable to the filter
type StringOrNumber = string | number | boolean;
type OnlyPrimitives = Extract<StringOrNumber, string | number>;
// string | number

// Exclude — remove union members assignable to the filter
type WithoutBooleans = Exclude<StringOrNumber, boolean>;
// string | number

// NonNullable — removes null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

// ReturnType and Parameters
function fetchUser(id: number, locale: string): Promise<{ name: string }> {
  return Promise.resolve({ name: "Alice" });
}

type FetchReturn = ReturnType<typeof fetchUser>;
// Promise<{ name: string }>

type FetchParams = Parameters<typeof fetchUser>;
// [id: number, locale: string]

// Awaited — unwraps nested Promises
type Resolved = Awaited<Promise<Promise<string>>>;  // string