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:
keyof T— produces a union of all keys in T[P in keyof T]— a mapped type: iterate over each key P?:— adds the optional modifier to each keyT[P]— preserves the original value type unchanged
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:
-?— the minus modifier strips the optional flag. The counterpart+?adds it (the+is usually omitted as default).
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:
readonly— the modifier makes each field assign-once at compile time.
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:
K extends keyof T— constrains K to be a subset of T's keys[P in K]— iterate only over the keys in KT[P]— preserves the original value type
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:
Exclude<keyof T, K>— removes K from T's key union, leaving the rest- Then Pick selects those remaining keys
- Omit is not a primitive mapped type — it's two utility types composed together
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:
K extends keyof any— K must be a valid object key (string | number | symbol)[P in K]: T— every key maps to the same value type T
// 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:
- These are conditional types (covered in depth next chapter)
T extends U ? never : T— if a member of T is assignable to U, discard it (returnnever); otherwise keep itneverin a union is automatically eliminated:string | never = string
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.