Skip to content
Codeloom
TypeScript

TypeScript Utility Types: The Complete Guide

A comprehensive guide to all built-in TypeScript utility types including Partial, Required, Pick, Omit, Record, Extract, Exclude, and more with practical examples.

·8 min read · By Codeloom
Intermediate 14 min read

What you'll learn

  • How every built-in TypeScript utility type works internally
  • When to use each utility type in real application code
  • How to combine utility types for complex transformations
  • How to build your own custom utility types

Prerequisites

  • TypeScript basics
  • Understanding of generics and mapped types

TypeScript ships with a set of built-in utility types that transform existing types into new ones. Instead of defining separate interfaces for every variation of your data, you can derive them from a single source of truth. This article covers every built-in utility type, explains how each one works internally, and shows when to use it.

Property Modifier Types

Partial<T>

Makes all properties of T optional:

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

function updateUser(id: number, updates: Partial<User>): void {
  // updates can have any combination of User fields
}

updateUser(1, { name: "Alice" });
updateUser(1, { email: "new@example.com", role: "admin" });
updateUser(1, {}); // also valid

How it works internally:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

The ? modifier makes every mapped property optional.

Required<T>

The opposite of Partial. Makes all properties required:

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

function startServer(config: Required<Config>): void {
  // All properties guaranteed to exist
  console.log(`Starting on ${config.host}:${config.port}`);
}

startServer({ host: "localhost", port: 3000, debug: false });
// startServer({ host: "localhost" }); // Error: missing port and debug

Internal implementation:

type Required<T> = {
  [P in keyof T]-?: T[P];
};

The -? modifier removes optionality.

Readonly<T>

Makes all properties read-only:

interface State {
  count: number;
  items: string[];
}

function createState(initial: State): Readonly<State> {
  return Object.freeze(initial);
}

const state = createState({ count: 0, items: [] });
// state.count = 1; // Error: Cannot assign to 'count' because it is a read-only property

Note that Readonly is shallow. Nested objects are still mutable:

state.items.push("item"); // No error - the array itself is mutable

For deep immutability, you need a recursive type:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

Property Selection Types

Pick<T, K>

Creates a type with only the specified properties:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

type PublicUser = Pick<User, "id" | "name" | "email">;

function sanitizeUser(user: User): PublicUser {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
  };
}

Omit<T, K>

Creates a type with all properties except the specified ones:

type CreateUserInput = Omit<User, "id" | "createdAt">;

function createUser(input: CreateUserInput): User {
  return {
    ...input,
    id: generateId(),
    createdAt: new Date(),
  };
}

createUser({ name: "Alice", email: "alice@example.com", password: "secure123" });

Pick and Omit are complementary. Use Pick when you want a small subset. Use Omit when you want most properties minus a few.

Record<K, T>

Creates an object type with keys of type K and values of type T:

type UserRole = "admin" | "editor" | "viewer";

const rolePermissions: Record<UserRole, string[]> = {
  admin: ["read", "write", "delete", "manage"],
  editor: ["read", "write"],
  viewer: ["read"],
};

Record is especially useful for lookup objects and dictionaries:

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",
};

Internal implementation:

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Union Manipulation Types

Extract<T, U>

Extracts from T the types that are assignable to U:

type AllEvents = "click" | "scroll" | "mousemove" | "keypress" | "keyup" | "keydown";

type MouseEvents = Extract<AllEvents, "click" | "scroll" | "mousemove">;
// "click" | "scroll" | "mousemove"

type KeyEvents = Extract<AllEvents, `key${string}`>;
// "keypress" | "keyup" | "keydown"

Exclude<T, U>

Removes from T the types that are assignable to U:

type NonKeyEvents = Exclude<AllEvents, `key${string}`>;
// "click" | "scroll" | "mousemove"

type NonNullable<T> = Exclude<T, null | undefined>;
// This is actually how NonNullable is implemented

Practical example for filtering union types:

type ApiEndpoint =
  | { method: "GET"; path: string; response: unknown }
  | { method: "POST"; path: string; body: unknown; response: unknown }
  | { method: "DELETE"; path: string; response: void };

type WriteMethods = Extract<ApiEndpoint, { method: "POST" | "DELETE" }>;

NonNullable<T>

Removes null and undefined from a type:

type MaybeUser = User | null | undefined;

function processUser(user: NonNullable<MaybeUser>): void {
  console.log(user.name); // safe, no null check needed
}

function getUser(id: number): MaybeUser {
  return users.get(id) ?? null;
}

const user = getUser(1);
if (user) {
  processUser(user); // type narrows to NonNullable<MaybeUser>
}

Function Types

ReturnType<T>

Extracts the return type of a function:

function createUser(name: string, email: string) {
  return {
    id: crypto.randomUUID(),
    name,
    email,
    createdAt: new Date(),
  };
}

type User = ReturnType<typeof createUser>;
// { id: string; name: string; email: string; createdAt: Date }

This is invaluable when you want to type something based on what a function returns without maintaining a separate interface:

async function fetchDashboardData() {
  const [users, orders, metrics] = await Promise.all([
    fetchUsers(),
    fetchOrders(),
    fetchMetrics(),
  ]);
  return { users, orders, metrics };
}

type DashboardData = Awaited<ReturnType<typeof fetchDashboardData>>;

Parameters<T>

Extracts the parameter types as a tuple:

function sendEmail(to: string, subject: string, body: string, cc?: string[]): void {
  // ...
}

type SendEmailParams = Parameters<typeof sendEmail>;
// [to: string, subject: string, body: string, cc?: string[]]

function queueEmail(...args: Parameters<typeof sendEmail>): void {
  emailQueue.push(args);
}

ConstructorParameters<T>

Like Parameters but for constructor functions:

class HttpClient {
  constructor(
    public baseUrl: string,
    public timeout: number = 5000,
    public headers: Record<string, string> = {}
  ) {}
}

type ClientConfig = ConstructorParameters<typeof HttpClient>;
// [baseUrl: string, timeout?: number, headers?: Record<string, string>]

function createClient(...args: ClientConfig): HttpClient {
  return new HttpClient(...args);
}

InstanceType<T>

Gets the instance type of a constructor function:

class UserRepository {
  findAll(): User[] { return []; }
  findById(id: number): User | null { return null; }
}

type Repo = InstanceType<typeof UserRepository>;
// UserRepository

Awaited<T>

Unwraps the resolved type of a Promise, recursively:

type A = Awaited<Promise<string>>;        // string
type B = Awaited<Promise<Promise<number>>>; // number
type C = Awaited<string | Promise<number>>; // string | number

This is what makes async/await type inference work correctly:

async function getData(): Promise<{ items: string[] }> {
  return { items: ["a", "b"] };
}

type Data = Awaited<ReturnType<typeof getData>>;
// { items: string[] }

String Manipulation Types

TypeScript includes types that transform string literal types:

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;        // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

These are most useful with template literal types:

type EventName = "click" | "scroll" | "resize";

type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onScroll" | "onResize"

type EventCallback<T extends EventName> = {
  [K in T as `on${Capitalize<K>}`]: (event: Event) => void;
};

type ClickHandler = EventCallback<"click">;
// { onClick: (event: Event) => void }

Combining Utility Types

The real power comes from composing utility types:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
  createdAt: Date;
  updatedAt: Date;
}

// For creating new users: no id, no timestamps
type CreateUser = Omit<User, "id" | "createdAt" | "updatedAt">;

// For updating: all fields optional except id
type UpdateUser = Partial<Omit<User, "id">> & Pick<User, "id">;

// For API responses: no password
type UserResponse = Omit<User, "password">;

// For lists: just the essentials
type UserSummary = Pick<User, "id" | "name" | "role">;

// For admin views: everything read-only
type AdminUserView = Readonly<UserResponse>;

Building Custom Utility Types

Once you understand how the built-in types work, you can build your own:

// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type CreateUser = PartialBy<User, "role" | "email">;
// id, name, password required; role, email optional

// Make specific properties required
type RequiredBy<T, K extends keyof T> = T & Required<Pick<T, K>>;

// Get only the properties that are functions
type Methods<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

// Get only non-function properties
type DataFields<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

// Make all properties mutable (remove readonly)
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

Wrapping Up

TypeScript’s utility types are mapped types and conditional types packaged for common use cases. Partial, Required, and Readonly modify property attributes. Pick and Omit select subsets of properties. Record creates object types from key and value types. Extract and Exclude filter union types. ReturnType, Parameters, and Awaited extract information from function and promise types. Combining these types lets you derive every variation of your data shapes from a single source definition, keeping your types DRY and consistent.