Type Utilities & Inference

TypeScript Intermediate — Type Utilities & Inference
TypeScript Intermediate
Course 2 · Chapter 7 · Type Utilities & Inference

🔧 Type Utilities & Inference

TypeScript's type system is powerful enough to build reusable type transformations. This chapter covers built-in utility types (Partial, Pick, Omit, Record, etc.), how to compose them, extract types from values, infer return types, and write type predicates. These tools unlock the full potential of the type system.

📚 Built-In Utility Types

TypeScript provides a standard library of utility types for common transformations:

interface User { id: number; name: string; email: string; age: number; } // Partial — all properties optional type PartialUser = Partial<User>; // { id?: number; name?: string; email?: string; age?: number; } // Required — all properties required type RequiredUser = Required<PartialUser>; // { id: number; name: string; email: string; age: number; } // Readonly — all properties readonly type ReadOnlyUser = Readonly<User>; // { readonly id: number; readonly name: string; ... } // Pick — select specific properties type UserPreview = Pick<User, "name" | "email">; // { name: string; email: string; } // Omit — exclude specific properties type UserWithoutPassword = Omit<User, "password">; // { id: number; name: string; email: string; age: number; } // Record — build object with specific keys type UserRoles = Record<"admin" | "user" | "guest", User[]>; // { admin: User[]; user: User[]; guest: User[]; }

Partial & Required

Toggle optionality. Partial<T> makes all optional; Required<T> makes all required.

Readonly

Make all properties immutable. Can't reassign after creation.

Pick & Omit

Pick selects properties; Omit excludes them. Slice a type.

Record

Build an object type from keys. Record<K, V> creates { [key in K]: V }.

🔍 Extract & Exclude: Filter Unions

Work with unions by keeping or removing types:

type StringOrNumber = string | number | boolean; // Extract — keep types assignable to U type Strings = Extract<StringOrNumber, string>; // string // Exclude — remove types assignable to U type NonStrings = Exclude<StringOrNumber, string>; // number | boolean // Practical: filter event types type Events = "click" | "focus" | "blur" | "change"; type FocusEvents = Extract<Events, "focus" | "blur">; // "focus" | "blur"

🔄 Extract Types From Functions

Infer function signatures to build related types:

function getUser(id: number): { id: number; name: string } { return { id, name: "Alice" }; } // ReturnType — extract return type type UserReturn = ReturnType<typeof getUser>; // { id: number; name: string } // Parameters — extract parameter types type GetUserParams = Parameters<typeof getUser>; // [id: number] // ConstructorParameters — for class constructors class User { constructor(name: string, age: number) { } } type UserConstructorParams = ConstructorParameters<typeof User>; // [name: string, age: number]

🎯 Type Predicates: Smart Narrowing

Custom type guards that narrow types intelligently:

interface Dog { bark(): void; } interface Cat { meow(): void; } // Type predicate: "value is Dog" tells TypeScript to narrow function isDog(animal: Dog | Cat): animal is Dog { return typeof (animal as Dog).bark === "function"; } const pet: Dog | Cat = getPet(); if (isDog(pet)) { pet.bark(); // ✅ TypeScript knows pet is Dog } else { pet.meow(); // ✅ TypeScript knows pet is Cat }

🔗 Composing Utility Types

Chain utilities to build complex transformations:

interface User { id: number; name: string; email: string; password: string; } // Exclude password, make all optional, make readonly type SafeUserPreview = Readonly<Partial<Omit<User, "password">>>; // Equivalent to: // { // readonly id?: number; // readonly name?: string; // readonly email?: string; // } // Use it for API responses const preview: SafeUserPreview = { name: "Alice", // email is optional, id is optional };

🏗️ Real-World Pattern: Generic API Wrapper

Build Type-Safe CRUD Operations

interface Entity { id: number; createdAt: Date; updatedAt: Date; } interface ApiResponse<T extends Entity> { status: "success" | "error"; data?: T; error?: string; } class Repository<T extends Entity> { // Create uses partial (no id, timestamps auto-generated) async create(data: Omit<T, "id" | "createdAt" | "updatedAt">): Promise<T> { // Simulate API call return { ...data, id: Math.random(), createdAt: new Date(), updatedAt: new Date() } as T; } // Update uses partial (update only what changed) async update(id: number, data: Partial<Omit<T, "id" | "createdAt">>): Promise<T> { // Update logic here return {} as T; } } // Usage with User type interface User extends Entity { name: string; email: string; } const userRepo = new Repository<User>(); // Create requires name and email, NOT id/timestamps await userRepo.create({ name: "Alice", email: "alice@example.com" }); // Update can be partial await userRepo.update(1, { name: "Bob" }); // ✅ Only name

💻 Coding Challenges

Challenge 1: Build Custom Utilities

Create utility types: GetType<T, K> (property type), Flatten<T> (array), Nullable<T> (add null to all properties).

Goal: Build reusable utility types using conditional, mapped, and utility type composition.

→ Solution

Challenge 2: Type Predicates

Write custom type guard functions: isString(), isArray<T>(), isRecord<T>(). Test them with discriminated unions.

Goal: Master type predicates and smart narrowing.

→ Solution

Challenge 3: Compose Utilities

Use Pick, Omit, Partial, Readonly together to build derived types. Create safe API response types from your domain types.

Goal: Combine utilities to solve real-world problems.

→ Solution

⚠️ Gotcha: Deep Transformations

Most built-in utilities (Partial, Readonly, etc.) work only at the top level. If you need to make nested properties optional, you'll build recursive utilities. That's advanced, but it's possible using conditional types and mapped types.

🎯 What's Next

With type utilities mastered, we'll explore Error Handling & Validation — custom error types, runtime validation, and exhaustiveness checking for bullet-proof code.