TypeScript Utility Types: Practical Code Snippets and Patterns

Handwritten mathematical formulas on a chalkboard

TypeScript ships with a set of built-in utility types that transform, narrow, and compose existing types into new ones. Instead of writing verbose type definitions by hand or duplicating interfaces every time you need a variation, utility types let you derive what you need from types you already have.

This reference covers the utility types that come up most often in real projects -- with code snippets you can adapt and patterns that explain not just how each type works but when to actually reach for it.

The Utility Types You Will Use Most

Partial and Required

Partial<T> makes every property of T optional. Required<T> does the opposite, making every property required even if the original had optionals.

interface UserProfile {
  id: string;
  name: string;
  email: string;
  bio?: string;
}

// All fields become optional -- useful for update payloads
type UserProfileUpdate = Partial<UserProfile>;

// All fields required -- useful for form validation
type UserProfileFull = Required<UserProfile>;

function updateUser(id: string, changes: Partial<UserProfile>) {
  // changes can contain any subset of UserProfile fields
}

Partial<T> is the most commonly used utility type and shows up constantly in API update handlers and form state types. One thing to be aware of: Partial<T> is shallow -- it only makes top-level properties optional. Nested objects stay as defined.

Pick and Omit

Pick<T, K> creates a type that includes only the keys you list. Omit<T, K> creates a type that excludes the keys you list. They're mirrors of each other.

interface Article {
  id: string;
  title: string;
  body: string;
  authorId: string;
  publishedAt: Date | null;
  tags: string[];
}

// Only the fields needed for a list view
type ArticlePreview = Pick<Article, 'id' | 'title' | 'publishedAt' | 'tags'>;

// Everything except the generated fields
type ArticleInput = Omit<Article, 'id' | 'publishedAt'>;

Omit<T, K> is particularly useful for request body types where you want most of an existing interface but need to exclude server-generated fields like id, createdAt, or updatedAt.

Readonly

Readonly<T> makes every property of T read-only. Attempting to reassign a property on a Readonly<T> object causes a type error.

interface Config {
  apiUrl: string;
  timeout: number;
  retryAttempts: number;
}

const defaultConfig: Readonly<Config> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retryAttempts: 3,
};

// This would be a type error:
// defaultConfig.timeout = 10000;

Readonly<T> is useful for configuration objects and constants where you want the compiler to enforce immutability rather than relying on conventions or comments.

Record

Record<K, V> constructs an object type with keys of type K and values of type V. It's cleaner than an index signature for cases where the key set is finite and known.

type Status = 'active' | 'inactive' | 'pending';

const statusLabels: Record<Status, string> = {
  active: 'Active',
  inactive: 'Inactive',
  pending: 'Pending Review',
};

// Exhaustive -- TypeScript will flag a missing key
type PermissionMap = Record<string, boolean>;

When the key type is a union of string literals, Record<K, V> enforces exhaustiveness -- you have to provide a value for every key in the union. This is a useful pattern for lookup tables, config maps, and anywhere you want the type checker to catch a missing case.

notebook open annotated pages desk diagram
Photo by Pixabay on Pexels

Utility Types for Narrowing Unions

Extract and Exclude

Extract<T, U> returns the members of T that are assignable to U. Exclude<T, U> returns the members that are not.

type AllEvents = 'click' | 'focus' | 'blur' | 'submit' | 'change';

// Only pointer/keyboard events
type InputEvents = Extract<AllEvents, 'focus' | 'blur' | 'change'>;
// => 'focus' | 'blur' | 'change'

// Everything except submit
type NonSubmitEvents = Exclude<AllEvents, 'submit'>;
// => 'click' | 'focus' | 'blur' | 'change'

These come up most often when working with string literal unions that describe event names, status values, or role types. They let you build derived subsets of a union without having to restate every member.

NonNullable

NonNullable<T> removes null and undefined from a type. It's useful when you know a value has been checked and want the compiler to stop treating it as potentially absent.

type MaybeUser = User | null | undefined;

type DefiniteUser = NonNullable<MaybeUser>;
// => User

function processUser(user: MaybeUser) {
  if (!user) return;
  const confirmed: DefiniteUser = user; // safe after the guard
}

Inferring Types from Functions

ReturnType and Parameters

ReturnType<T> extracts the return type of a function type T. Parameters<T> extracts the parameter types as a tuple.

function fetchProduct(id: string, locale: string): Promise<Product> {
  // ...
}

type FetchResult = ReturnType<typeof fetchProduct>;
// => Promise<Product>

type FetchArgs = Parameters<typeof fetchProduct>;
// => [id: string, locale: string]

ReturnType<T> is particularly useful when the return type of a function is complex or inferred. Instead of duplicating that type manually or importing it from deep in the codebase, you can derive it directly from the function. This keeps types in sync automatically when the function signature changes.

Parameters<T> is helpful for wrapper functions and higher-order functions where you want to forward arguments without redeclaring their types.

function withLogging<T extends (...args: unknown[]) => unknown>(fn: T) {
  return (...args: Parameters<T>): ReturnType<T> => {
    console.log('Calling', fn.name, 'with', args);
    return fn(...args) as ReturnType<T>;
  };
}

"TypeScript utility types are one of those things that seem academic until you're maintaining a large codebase. Once you use them systematically, going back to manual type duplication feels like giving up version control." - Dennis Traina, founder of 137Foundry

Composing Utility Types

Utility types compose cleanly. You can pass the output of one as the input to another to build up exactly the type shape you need.

interface Product {
  id: string;
  sku: string;
  name: string;
  price: number;
  stock: number;
  publishedAt: Date | null;
  deletedAt: Date | null;
}

// Writable update payload: no id, no deletedAt, all optional
type ProductUpdate = Partial<Omit<Product, 'id' | 'deletedAt'>>;

// Read-only snapshot for cache storage
type CachedProduct = Readonly<Pick<Product, 'id' | 'sku' | 'name' | 'price'>>;

terminal screen monospace green display close
Photo by Oscar Chan on Pexels

Composing utility types is cleaner than writing out each variant by hand and keeps definitions in sync automatically when the source interface changes. If Product gains a new field, ProductUpdate and CachedProduct update to reflect it without any manual edits.

A common pattern for API layers is to define the full database model once and then derive every variant from it: creation input, update payload, public response shape, and internal admin view -- all derived using Omit, Pick, Partial, and Required from a single source of truth.

Writing Custom Utility Types

The built-in utility types are themselves built from conditional types. Once you understand the pattern, you can write your own.

// Make specific keys optional while keeping the rest required
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface CreatePostInput {
  title: string;
  body: string;
  slug: string;
  publishedAt: Date;
}

// slug and publishedAt are optional; title and body are required
type DraftPostInput = PartialBy<CreatePostInput, 'slug' | 'publishedAt'>;
// Deep partial -- makes nested properties optional too
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Settings {
  theme: {
    color: string;
    font: string;
  };
  notifications: {
    email: boolean;
    push: boolean;
  };
}

type SettingsUpdate = DeepPartial<Settings>;
// theme.color, notifications.email, etc. all become optional

Custom utility types follow the same pattern as built-ins: a generic type parameter, a conditional or mapped type expression, and optionally a constraint on the parameter. TypeScript's handbook documents the full conditional type syntax if you want to go deeper.

server rack cables organized data center
Photo by Brett Sayles on Pexels

Where These Patterns Help Most in Production Code

Utility types reduce type duplication, which reduces the maintenance cost of keeping types in sync as code evolves. The places where they help most:

API layers. Define one canonical model type, then derive request and response shapes from it. Changes to the model automatically propagate to derived types rather than requiring manual updates in multiple files.

Form handling. Partial<T> for in-progress form state, Required<T> for validated submission payloads. React applications and most modern form libraries benefit from this pattern to avoid managing separate type definitions for form state and submission data.

Utilities and wrappers. ReturnType<T> and Parameters<T> for functions that wrap or delegate to other functions. The wrapper stays type-accurate even as the wrapped function's signature evolves.

Configuration objects. Readonly<T> for config structures you want to protect from accidental mutation, plus Record<K, V> for lookup tables where exhaustiveness over a known key set matters.

Testing and mocking. Partial<T> and Pick<T, K> are useful for constructing test fixtures that only include the fields a given test cares about. Rather than building full objects with every required field, you can build minimal objects that satisfy the type for the specific function under test. This reduces test boilerplate and makes it easier to see which fields are actually relevant to the behavior being tested.

These types are part of the standard TypeScript distribution and require no additional packages. MDN Web Docs covers the underlying JavaScript patterns that some of these compile down to, which is useful context when debugging the runtime behavior of types that involve mapped object shapes.

If your team is building complex type-safe systems or TypeScript architectures from scratch, the web development services at 137Foundry include TypeScript application architecture and code review. More context on our approach to technology decisions is on the 137Foundry services page. If you want to learn more about who builds these kinds of systems, the 137Foundry homepage has the full overview.

Need help with Web Development?

137Foundry builds custom software, AI integrations, and automation systems for businesses that need real solutions.

Book a Free Consultation View Services