XplormityXplormity
HomeHandbooks
Browse
Xplormity

TLDR developer handbooks for
seasoned developers.

Handbooks

RustNestJSNext.jsGitDockerTypeScriptReactNode.jsDSASQLSystem DesignTailwind CSS

Site

HomeHandbooksAboutPrivacyTerms

Connect

GitHubTwitterLinkedIn

© 2026 Xplormity. All rights reserved.

HandbooksTypeScriptUtility Types Deep Dive

Utility Types Deep Dive

utility-typesadvancedhandbook

TL;DR

  • TypeScript ships 20+ built-in utility types. Knowing how they work (not just what they do) is key.
  • Most are implemented with mapped types + conditional types.
  • satisfies (TS 4.9+) validates a value matches a type without widening it.
  • const assertions freeze literal types.

Step 1: Object Transformation Utilities

Object transformation utilities (Partial, Required, Readonly) exist because the same data shape often needs different "modes" throughout an application: a user creation form needs all fields optional (partial input), the database record needs all fields required, and the API response should be readonly to prevent accidental mutation. Without these utilities, you'd duplicate type definitions for each variant. They're the most-used TypeScript utilities and appear in every codebase that handles forms, API responses, or state management.

Partial — Make all properties optional

// Implementation
type Partial<T> = { [K in keyof T]?: T[K] };

// Use case: Update functions where you only pass changed fields
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, changes: Partial<User>) {
  // changes can have any subset of User properties
}

updateUser("1", { name: "Bob" });          // ✅
updateUser("1", { name: "Bob", age: 26 }); // ✅
updateUser("1", {});                        // ✅

Required — Make all properties required

// Implementation
type Required<T> = { [K in keyof T]-?: T[K] };
// The -? removes the optional modifier

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

// After defaults are applied, everything is guaranteed
const finalConfig: Required<Config> = {
  host: "localhost",
  port: 3000,
  debug: false,
};

Readonly & DeepReadonly

// Built-in Readonly (shallow only)
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// Deep readonly — common interview question
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]  // Don't make functions readonly
      : DeepReadonly<T[K]>
    : T[K];
};

interface Nested {
  user: {
    name: string;
    address: {
      city: string;
    };
  };
}

type Frozen = DeepReadonly<Nested>;
// user.name, user.address.city are all readonly — can't mutate at any depth

Step 2: Key Selection Utilities

Pick and Omit were created because real-world APIs almost never want the full object — a form needs 5 of 20 fields, a list view needs name and avatar but not the full profile, an API response should exclude internal fields. Without these utilities, you'd write manual interfaces for every "view" of your data. They enforce that selected keys actually exist on the source type, catching typos at compile time. These are among the most-used utilities in any TypeScript codebase.

Pick<T, K> — Select specific properties

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

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

// API response — never expose password
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

Omit<T, K> — Remove specific properties

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

// Remove sensitive fields
type SafeUser = Omit<User, "password">;

// Create DTO without id (server generates it)
type CreateUserDto = Omit<User, "id" | "createdAt">;

Record<K, V> — Object with known key/value types

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

// Permission map
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;

const perms: Permissions = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
};

// Indexed state map
type LoadingState = Record<string, { loading: boolean; error?: string }>;

Step 3: Union Utilities

Union utilities (Exclude, Extract, NonNullable) manipulate union types the way filter/map work on arrays. They were needed because union types are central to TypeScript (every string | null, every discriminated union, every enum-like const) and you frequently need to add/remove members programmatically. Exclude removes types from a union, Extract keeps only matching types, and NonNullable strips null | undefined. They're the building blocks for creating domain-specific utility types.

Exclude<T, U> — Remove members from union

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

type Status = "pending" | "active" | "banned" | "deleted";
type ActiveStatuses = Exclude<Status, "banned" | "deleted">;
// "pending" | "active"

Extract<T, U> — Keep only matching members

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

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

NonNullable — Remove null and undefined

type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string

Step 4: Function Utilities

Function utilities (Parameters, ReturnType, Awaited) extract type information from function signatures. They were invented because in real codebases you often need to type things based on existing functions without duplicating their signatures: "this variable has the same type as what fetchUser returns" or "these args match what createHandler expects." They use infer under the hood to pull types out of function shapes, and they're essential for wrapper functions, middleware, and testing utilities.

Parameters — Extract parameter types as tuple

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

function createUser(name: string, age: number, admin: boolean) { /* ... */ }

type CreateUserParams = Parameters<typeof createUser>;
// [string, number, boolean]

// Useful for wrapping functions
function withLogging<T extends (...args: any[]) => any>(
  fn: T,
  ...args: Parameters<T>
): ReturnType<T> {
  console.log("Calling with:", args);
  return fn(...args);
}

ReturnType — Extract return type

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

async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as Promise<User>;
}

type FetchResult = Awaited<ReturnType<typeof fetchUser>>; // User

ConstructorParameters — Extract constructor args

class UserService {
  constructor(private db: Database, private logger: Logger) {}
}

type ServiceDeps = ConstructorParameters<typeof UserService>;
// [Database, Logger]

Step 5: The satisfies Operator (TS 4.9+)

satisfies was the most-requested TypeScript feature for years because developers faced an impossible choice: type annotations give validation but widen types (losing literal inference), while no annotation preserves literals but doesn't validate. satisfies gives you both: it checks that your value matches a type without changing what TypeScript infers. This is perfect for configuration objects, route maps, and any constant where you want both type-safety AND precise autocomplete on the values.

satisfies validates that a value conforms to a type without widening the inferred type:

// ❌ Problem with type annotation — widens to Record<string, string | number>
const config: Record<string, string | number> = {
  host: "localhost",
  port: 3000,
};
config.host.toUpperCase(); // ❌ Error: might be number

// ✅ With satisfies — validates shape but keeps narrow literal types
const config = {
  host: "localhost",
  port: 3000,
} satisfies Record<string, string | number>;

config.host.toUpperCase(); // ✅ TS knows it's "localhost" (string)
config.port.toFixed(2);    // ✅ TS knows it's 3000 (number)

// Practical: Route definitions
type Route = { path: string; method: "GET" | "POST" | "PUT" | "DELETE" };

const routes = {
  getUser: { path: "/users/:id", method: "GET" },
  createUser: { path: "/users", method: "POST" },
} satisfies Record<string, Route>;

// routes.getUser.method is "GET" not just string — narrowest possible type!

Step 6: const Assertions

as const was added because TypeScript's type widening (inferring string instead of "hello") is usually helpful but sometimes wrong. When you define a config object or enum-like constant, you want TypeScript to know the exact values, not just their types. Without as const, ["admin", "user"] is string[]; with it, it's readonly ["admin", "user"] — and you can derive a union type "admin" | "user" from it. This pattern eliminates the need for TypeScript enums in most cases.

as const makes TypeScript infer the narrowest possible type:

// Without as const
const colors = ["red", "green", "blue"];
// type: string[]

// With as const
const colors = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]

// Now you can derive types from values
type Color = (typeof colors)[number]; // "red" | "green" | "blue"

// Object with as const
const HTTP_STATUS = {
  OK: 200,
  NOT_FOUND: 404,
  SERVER_ERROR: 500,
} as const;

type StatusCode = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
// 200 | 404 | 500

// Enum alternative (lighter weight)
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction = (typeof Direction)[keyof typeof Direction];
// "UP" | "DOWN" | "LEFT" | "RIGHT"

Step 7: Building Custom Utility Types

Custom utility types are what make TypeScript scale in large codebases. The built-in utilities cover generic patterns, but every domain has its own: DeepPartial for nested form updates, Prettify for readable hover tooltips, StrictOmit that errors on invalid keys. Building these requires combining generics, conditional types, mapped types, and infer — all the advanced features working together. This is the level of TypeScript knowledge that library authors and senior engineers operate at.

DeepPartial — Recursively make optional

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      user: string;
      password: string;
    };
  };
}

// Can pass any nested subset
function configure(overrides: DeepPartial<Config>) { /* ... */ }
configure({ database: { port: 5433 } }); // ✅

PickByType — Filter properties by value type

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  active: boolean;
  email: string;
}

type StringProps = PickByType<Mixed, string>;
// { name: string; email: string }

MakeRequired — Make specific keys required

type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

interface FormData {
  name?: string;
  email?: string;
  phone?: string;
}

type SubmitData = MakeRequired<FormData, "name" | "email">;
// { name: string; email: string; phone?: string }

Interview Questions

  1. What's the difference between Partial and DeepPartial?

    • Partial only makes top-level properties optional. Nested objects remain fully required. DeepPartial recursively applies optionality at every depth.
  2. When would you use satisfies vs a type annotation?

    • Use satisfies when you want validation without losing narrow types. Use annotations when you explicitly want the wider type.
  3. How do you create a type from runtime values?

    • Use as const to freeze the value, then typeof to extract the type. Example: const x = [...] as const; type X = typeof x[number];
  4. What's the difference between Record<string, T> and { [key: string]: T }?

    • Functionally identical. Record is more readable and composable with other utilities.
Advanced Generics & Conditional TypesTypeScript 7.0 — The Go Rewrite

On this page

  • TL;DR
  • Step 1: Object Transformation Utilities
  • Partial<T> — Make all properties optional
  • Required<T> — Make all properties required
  • Readonly<T> & DeepReadonly
  • Step 2: Key Selection Utilities
  • Pick<T, K> — Select specific properties
  • Omit<T, K> — Remove specific properties
  • Record<K, V> — Object with known key/value types
  • Step 3: Union Utilities
  • Exclude<T, U> — Remove members from union
  • Extract<T, U> — Keep only matching members
  • NonNullable<T> — Remove null and undefined
  • Step 4: Function Utilities
  • Parameters<T> — Extract parameter types as tuple
  • ReturnType<T> — Extract return type
  • ConstructorParameters<T> — Extract constructor args
  • Step 5: The satisfies Operator (TS 4.9+)
  • Step 6: const Assertions
  • Step 7: Building Custom Utility Types
  • DeepPartial — Recursively make optional
  • PickByType — Filter properties by value type
  • MakeRequired — Make specific keys required
  • Interview Questions