Chapter 7

Built-in Utility Types Dissected: Source Code Walkthrough

Why Read the Source Code

TypeScript's built-in utility types are defined in lib.es5.d.ts โ€” the source is public and readable. Understanding them isn't just about knowing how to use them. It's about understanding how mapped types and conditional types compose, so you can write your own utility types when the built-ins don't cover your case.

# Find the TypeScript type definitions
node_modules/typescript/lib/lib.es5.d.ts
# Or Cmd+Click any utility type name in your IDE to jump to the definition

Partial<T> โ€” Make Every Field Optional

Source:

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

How it works:

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

// Partial<User> expands to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   role?: "admin" | "user";
// }

// Classic use case: update functions that only send changed fields
function updateUser(id: number, patch: Partial<User>): User {
  const existing = findUserById(id);
  return { ...existing, ...patch };
}

updateUser(1, { name: "Bob" });                    // only update name
updateUser(1, { email: "[email protected]", role: "admin" }); // update multiple fields

Deep Partial (not in stdlib โ€” roll your own):

// Standard Partial only goes one level deep
interface DeepNested {
  a: {
    b: {
      c: string;
    };
  };
}

// Partial<DeepNested> makes `a` optional but `a.b.c` stays required
type StdPartial = Partial<DeepNested>;
// { a?: { b: { c: string } } }

// Recursive DeepPartial
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

Required<T> โ€” Make Every Field Mandatory

Source:

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

How it works:

interface FormData {
  name?: string;
  email?: string;
  age?: number;
}

// Required<FormData> expands to:
// { name: string; email: string; age: number }

// Classic use case: after validation, data transitions from "maybe present" to "definitely present"
function validateForm(data: FormData): Required<FormData> {
  if (!data.name) throw new Error("name required");
  if (!data.email) throw new Error("email required");
  if (!data.age) throw new Error("age required");
  return data as Required<FormData>; // assertion is safe after the checks above
}

function submitForm(data: Required<FormData>) {
  console.log(data.name.toUpperCase()); // safe โ€” name is guaranteed to exist
}

Readonly<T> โ€” Make Every Field Immutable

Source:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

How it works:

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

const config: Readonly<Config> = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  retries: 3,
};

config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property

// Readonly is shallow โ€” it does not recurse into nested objects
const nested: Readonly<{ inner: { x: number } }> = { inner: { x: 1 } };
nested.inner = { x: 2 }; // Error: inner is read-only
nested.inner.x = 99;     // OK: Readonly doesn't protect nested properties

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

Pick<T, K> โ€” Select Specific Fields

Source:

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

How it works:

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

// Select only public-safe fields
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

// Classic use case: DTO (Data Transfer Object) โ€” never send password to the client

function getUserProfile(id: number): PublicUser {
  const user = findUserById(id);
  return {
    id: user.id,
    name: user.name,
    email: user.email,
  };
}

// Dynamic field selection
function selectFields<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(k => { result[k] = obj[k]; });
  return result;
}

const summary = selectFields(user, ["id", "name"]);
// type: Pick<User, "id" | "name">

Omit<T, K> โ€” Exclude Specific Fields

Source (composed from Pick + Exclude):

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

How it works:

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

// Remove the password field
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; role: "admin" | "user" }

// Remove multiple fields
type CreateUserDto = Omit<User, "id" | "createdAt">;
// Creating a user doesn't need id (server generates it)

// Pick vs Omit โ€” how to choose:
// - Keeping a small number of fields โ†’ Pick (explicit about what you want)
// - Removing a small number of fields โ†’ Omit (explicit about what you don't want)
// - Many fields, removing 1-2 โ†’ Omit is easier to maintain

// Watch out: Omit's K is not strictly constrained to keyof T
interface Base {
  a: string;
  b: number;
}
type Weird = Omit<Base, "c">; // No error! "c" is not in Base but Omit doesn't complain
// Because K extends keyof any, not K extends keyof T

// Stricter version:
type StrictOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type Strict = StrictOmit<Base, "c">; // Error: 'c' does not exist in keyof Base

Record<K, V> โ€” Build Key-Value Map Types

Source:

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

How it works:

// Lookup table with literal key union
type CountryCode = "US" | "CN" | "JP" | "DE";
type CountryName = Record<CountryCode, string>;

const countries: CountryName = {
  US: "United States",
  CN: "China",
  JP: "Japan",
  DE: "Germany",
};

// HTTP status code mapping
const httpMessages: Record<number, string> = {
  200: "OK",
  201: "Created",
  400: "Bad Request",
  401: "Unauthorized",
  404: "Not Found",
  500: "Internal Server Error",
};

// Generic cache / dictionary
type Cache<T> = Record<string, T>;
const userCache: Cache<User> = {};

function getUser(id: string): User | undefined {
  return userCache[id];
}

// Combine with Partial for optional lookup tables
type PartialRecord<K extends keyof any, V> = Partial<Record<K, V>>;

type RolePermissions = PartialRecord<"read" | "write" | "delete", boolean>;
const adminPerms: RolePermissions = { read: true, write: true }; // delete can be omitted

Exclude<T, U> and Extract<T, U> โ€” Filter Union Members

Source:

type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;

How it works:

type Status = "pending" | "active" | "suspended" | "deleted";

// Remove specific members
type ActiveStatus = Exclude<Status, "deleted" | "suspended">;
// "pending" | "active"

// Keep only specific members
type TerminalStatus = Extract<Status, "suspended" | "deleted">;
// "suspended" | "deleted"

// Practical: strip function types from a union
type NonFunction<T> = Exclude<T, Function>;
type Primitives = NonFunction<string | number | (() => void)>;
// string | number

// Note: Omit's internals use Exclude:
// Omit<T, K> = Pick<T, Exclude<keyof T, K>>

NonNullable<T> โ€” Strip null and undefined

Source:

type NonNullable<T> = T & {};
// Pre-TS-4.8 implementation:
// type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string

// Classic use case: assert that a potentially nullable value is present
interface ApiResponse {
  data: User | null;
  error: string | null;
}

function assertData(res: ApiResponse): NonNullable<ApiResponse["data"]> {
  if (!res.data) throw new Error(res.error ?? "No data");
  return res.data; // TypeScript narrows this to User automatically
}

ReturnType<T>, Parameters<T>, InstanceType<T> โ€” Extracting Type Info

Source:

type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

type Parameters<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

type InstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : any;

All three use infer (covered in detail next chapter). Here's the usage pattern:

// ReturnType: when you can't import the return type directly
function createStore() {
  return {
    state: { count: 0, user: null as User | null },
    dispatch: (action: string) => { /* ... */ },
    getState: () => ({ count: 0 }),
  };
}

type Store = ReturnType<typeof createStore>;
// { state: { count: number; user: User | null }; dispatch: (action: string) => void; ... }

// Parameters: get a function's parameter types as a tuple
function login(username: string, password: string, remember: boolean): void {}

type LoginParams = Parameters<typeof login>;
// [username: string, password: string, remember: boolean]

type FirstParam = Parameters<typeof login>[0]; // string

// InstanceType: get the instance type from a constructor
class EventEmitter {
  on(event: string, handler: Function): void {}
  emit(event: string, ...args: unknown[]): void {}
}

type Emitter = InstanceType<typeof EventEmitter>;
// EventEmitter โ€” equivalent to writing EventEmitter directly,
// but more useful with abstract classes and Mixins

Real-World Patterns

Partial Update (PATCH endpoint)

interface UserProfile {
  id: number;
  name: string;
  bio: string;
  avatar: string;
  website: string;
}

// Create: no id needed (server generates it)
type CreateProfileDto = Omit<UserProfile, "id">;

// Update: id is required to identify the record, all other fields optional
type UpdateProfileDto = Pick<UserProfile, "id"> & Partial<Omit<UserProfile, "id">>;

async function updateProfile(dto: UpdateProfileDto): Promise<UserProfile> {
  const current = await fetchProfile(dto.id);
  return { ...current, ...dto };
}

await updateProfile({ id: 1, bio: "New bio" });
await updateProfile({ id: 1, avatar: "new-avatar.png", website: "https://example.com" });

API Response DTO โ€” Hide Sensitive Fields

interface UserRecord {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  salt: string;
  loginAttempts: number;
  lastLoginIp: string;
}

// Public-facing fields
type UserDto = Pick<UserRecord, "id" | "name" | "email">;

// Admin view: more fields, but still no password data
type AdminUserDto = Omit<UserRecord, "passwordHash" | "salt">;

Config with Defaults

interface ServerConfig {
  host: string;
  port: number;
  https: boolean;
  timeout: number;
  maxConnections: number;
}

const defaults: Readonly<ServerConfig> = {
  host: "0.0.0.0",
  port: 3000,
  https: false,
  timeout: 30000,
  maxConnections: 100,
};

function createServer(options: Partial<ServerConfig>): ServerConfig {
  return { ...defaults, ...options };
}

const server = createServer({ port: 8080, https: true });
// remaining fields come from defaults

Anti-Pattern: Re-Implementing What Utility Types Already Do

// Anti-pattern: manually writing a Partial-like interface
interface MyPartialUser {
  id?: number;
  name?: string;
  email?: string;
}
// Every time User gains a new field, this must be manually updated โ€” a maintenance nightmare

// Correct: Partial<User> stays in sync automatically
type UpdatePayload = Partial<User>;

// Anti-pattern: manually copying a subset of fields into a new interface
interface UserSummary {
  id: number;
  name: string;
}
// Same synchronization problem

// Correct:
type UserSummary = Pick<User, "id" | "name">;

// Anti-pattern: using type assertions to bypass the type system
const safeUser = user as { id: number; name: string };
// No type checking โ€” if User.email is renamed, this won't error

// Correct:
const safeUser: Pick<User, "id" | "name"> = { id: user.id, name: user.name };

Summary

Utility Type Core Mechanism Typical Use Case
Partial<T> [P in keyof T]? update/patch function parameters
Required<T> [P in keyof T]-? post-validation data types
Readonly<T> readonly [P in keyof T] config objects, constants
Pick<T, K> [P in K]: T[P] DTOs, exposing select fields
Omit<T, K> Pick<T, Exclude<keyof T, K>> hiding sensitive fields
Record<K, V> [P in K]: V lookup tables, caches, dictionaries
Exclude<T, U> T extends U ? never : T filter out union members
Extract<T, U> T extends U ? T : never keep matching union members
NonNullable<T> T & {} strip null/undefined
ReturnType<T> infer R infer a function's return type
Parameters<T> infer P infer a function's parameter types

Next chapter: conditional types and the infer keyword โ€” the full technique for extracting subtypes from any type.

Rate this chapter
4.6  / 5  (50 ratings)

๐Ÿ’ฌ Comments