TypeScript

Advanced Utility Type Patterns

32 min Lesson 33 of 40

Advanced Utility Type Patterns

TypeScript's utility types are powerful building blocks, but the real magic happens when you combine and extend them to create sophisticated custom type transformations. In this lesson, we'll explore advanced utility type patterns including DeepPartial, DeepReadonly, PathKeys for nested object access, and techniques for building reusable type utilities that solve complex typing challenges.

Why Build Custom Utility Types?

Custom utility types enable you to:

  • Reduce Repetition: Define complex type transformations once and reuse them throughout your codebase
  • Enforce Consistency: Standardize how types are transformed across your application
  • Improve Maintainability: Centralize type logic in reusable utilities rather than duplicating it
  • Express Intent: Create self-documenting type names that clearly communicate purpose
  • Handle Edge Cases: Build utilities that correctly handle complex nested structures

DeepPartial - Recursive Optional Properties

The built-in Partial<T> only makes top-level properties optional. DeepPartial recursively makes all nested properties optional:

DeepPartial Implementation:
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};

// Example usage
interface User {
  id: number;
  profile: {
    firstName: string;
    lastName: string;
    address: {
      street: string;
      city: string;
      country: string;
    };
  };
  preferences: {
    theme: 'light' | 'dark';
    notifications: {
      email: boolean;
      push: boolean;
    };
  };
}

// All properties at all levels become optional
type PartialUser = DeepPartial<User>;

const updateUser: PartialUser = {
  profile: {
    address: {
      city: 'New York', // Can update just one nested property
    },
  },
};
Note: DeepPartial is especially useful for update operations where you want to allow partial updates to deeply nested objects without requiring all properties at every level.

DeepReadonly - Recursive Immutability

Make all properties at all nesting levels readonly to enforce complete immutability:

DeepReadonly Implementation:
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? ReadonlyArray<DeepReadonly<U>>
      : DeepReadonly<T[P]>
    : T[P];
};

// Example usage
interface Config {
  api: {
    baseURL: string;
    timeout: number;
    headers: {
      authorization: string;
      contentType: string;
    };
  };
  features: {
    darkMode: boolean;
    analytics: boolean;
  };
}

type ImmutableConfig = DeepReadonly<Config>;

const config: ImmutableConfig = {
  api: {
    baseURL: 'https://api.example.com',
    timeout: 5000,
    headers: {
      authorization: 'Bearer token',
      contentType: 'application/json',
    },
  },
  features: {
    darkMode: true,
    analytics: false,
  },
};

// TypeScript error: Cannot assign to 'baseURL' because it is a read-only property
// config.api.baseURL = 'https://new-api.com';

// TypeScript error: Cannot assign to 'darkMode' because it is a read-only property
// config.features.darkMode = false;

DeepRequired - Recursive Required Properties

Make all optional properties required at all nesting levels:

DeepRequired Implementation:
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepRequired<U>>
      : DeepRequired<T[P]>
    : T[P];
};

// Example with optional nested properties
interface PartialForm {
  username?: string;
  profile?: {
    bio?: string;
    social?: {
      twitter?: string;
      github?: string;
    };
  };
}

type CompleteForm = DeepRequired<PartialForm>;

// TypeScript error: All properties must be provided
const form: CompleteForm = {
  username: 'john_doe',
  profile: {
    bio: 'Developer',
    social: {
      twitter: '@johndoe',
      github: 'johndoe',
    },
  },
};
Tip: The -? modifier removes the optional flag from properties. This is the opposite of ? which adds the optional flag.

PathKeys - Type-Safe Object Path Access

Generate string literal types representing all valid paths through a nested object:

PathKeys Implementation:
type PathKeys<T> = T extends object
  ? {
      [K in keyof T]: K extends string
        ? T[K] extends object
          ? `${K}` | `${K}.${PathKeys<T[K]>}`
          : `${K}`
        : never;
    }[keyof T]
  : never;

// Example usage
interface Settings {
  user: {
    name: string;
    email: string;
    preferences: {
      theme: string;
      language: string;
    };
  };
  app: {
    version: string;
    features: {
      darkMode: boolean;
      notifications: boolean;
    };
  };
}

type SettingPath = PathKeys<Settings>;
// Result type:
// "user" | "user.name" | "user.email" | "user.preferences" |
// "user.preferences.theme" | "user.preferences.language" |
// "app" | "app.version" | "app.features" |
// "app.features.darkMode" | "app.features.notifications"

// Type-safe getter function
function getNestedValue<T, P extends PathKeys<T>>(
  obj: T,
  path: P
): any {
  return path.split('.').reduce((acc: any, key) => acc?.[key], obj);
}

const settings: Settings = {
  user: {
    name: 'John',
    email: 'john@example.com',
    preferences: {
      theme: 'dark',
      language: 'en',
    },
  },
  app: {
    version: '1.0.0',
    features: {
      darkMode: true,
      notifications: false,
    },
  },
};

const theme = getNestedValue(settings, 'user.preferences.theme'); // Valid
// const invalid = getNestedValue(settings, 'user.invalid.path'); // TypeScript error

Mutable - Remove Readonly Modifiers

Create a writable version of a readonly type:

Mutable Implementation:
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

// Deep version
type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepMutable<U>>
      : DeepMutable<T[P]>
    : T[P];
};

// Example
interface ReadonlyUser {
  readonly id: number;
  readonly name: string;
  readonly profile: {
    readonly bio: string;
  };
}

type WritableUser = DeepMutable<ReadonlyUser>;

const user: WritableUser = {
  id: 1,
  name: 'John',
  profile: { bio: 'Developer' },
};

user.name = 'Jane'; // Allowed
user.profile.bio = 'Designer'; // Allowed
Warning: Use Mutable sparingly. Readonly types exist for a reason - removing readonly modifiers can compromise immutability guarantees that other parts of your codebase rely on.

PickByType - Select Properties by Value Type

Create a type containing only properties whose values match a specific type:

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

// Opposite: OmitByType
type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

// Example usage
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
  rating: number;
  category: string;
}

// Get only string properties
type StringProps = PickByType<Product, string>;
// Result: { name: string; description: string; category: string }

// Get only number properties
type NumberProps = PickByType<Product, number>;
// Result: { id: number; price: number; rating: number }

// Get only non-string properties
type NonStringProps = OmitByType<Product, string>;
// Result: { id: number; price: number; inStock: boolean; rating: number }

RequireAtLeastOne - Require One of Many Properties

Ensure at least one property from a set is provided:

RequireAtLeastOne Implementation:
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> &
      Partial<Pick<T, Exclude<Keys, K>>>;
  }[Keys];

// Example usage
interface ContactInfo {
  name: string;
  email?: string;
  phone?: string;
  address?: string;
}

type ContactRequired = RequireAtLeastOne<
  ContactInfo,
  'email' | 'phone' | 'address'
>;

// Valid: has email
const contact1: ContactRequired = {
  name: 'John',
  email: 'john@example.com',
};

// Valid: has phone
const contact2: ContactRequired = {
  name: 'Jane',
  phone: '555-0123',
};

// Valid: has multiple
const contact3: ContactRequired = {
  name: 'Bob',
  email: 'bob@example.com',
  phone: '555-0456',
};

// TypeScript error: must have at least one of email, phone, or address
// const invalid: ContactRequired = {
//   name: 'Invalid',
// };

RequireExactlyOne - Require Exactly One Property

Ensure exactly one property from a set is provided (mutually exclusive properties):

RequireExactlyOne Implementation:
type RequireExactlyOne<T, Keys extends keyof T = keyof T> = Pick<
  T,
  Exclude<keyof T, Keys>
> &
  {
    [K in Keys]-?: Required<Pick<T, K>> &
      Partial<Record<Exclude<Keys, K>, undefined>>;
  }[Keys];

// Example usage
interface SearchParams {
  query?: string;
  userId?: number;
  email?: string;
}

type ExactSearch = RequireExactlyOne<
  SearchParams,
  'query' | 'userId' | 'email'
>;

// Valid: exactly one property
const search1: ExactSearch = { query: 'typescript' };
const search2: ExactSearch = { userId: 123 };
const search3: ExactSearch = { email: 'user@example.com' };

// TypeScript error: cannot provide multiple properties
// const invalid1: ExactSearch = { query: 'ts', userId: 1 };

// TypeScript error: must provide exactly one property
// const invalid2: ExactSearch = {};

Prettify - Flatten Intersection Types for Better IDE Display

TypeScript intersection types can be hard to read in IDE tooltips. Prettify flattens them:

Prettify Implementation:
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

// Example
type Base = { id: number; name: string };
type Extended = Base & { email: string; age: number };

// Without Prettify, IDE shows: Base & { email: string; age: number }
type Ugly = Extended;

// With Prettify, IDE shows: { id: number; name: string; email: string; age: number }
type Pretty = Prettify<Extended>;
Tip: Use Prettify when creating complex utility types to make IDE hover tooltips more readable. It doesn't change the type behavior, only how it's displayed.

PartialBy - Make Specific Properties Optional

Make only specific properties optional while keeping others required:

PartialBy Implementation:
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Example
interface User {
  id: number;
  email: string;
  username: string;
  firstName: string;
  lastName: string;
}

// Make only email and username optional
type UserUpdate = PartialBy<User, 'email' | 'username'>;

const update: UserUpdate = {
  id: 1,
  firstName: 'John',
  lastName: 'Doe',
  // email and username are optional
};

Building a Comprehensive Utility Type Library

Combine utilities into a reusable library:

Complete Utility Library:
// types/utils.ts

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[P]>
    : T[P];
};

export type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? ReadonlyArray<DeepReadonly<U>>
      : DeepReadonly<T[P]>
    : T[P];
};

export type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepRequired<U>>
      : DeepRequired<T[P]>
    : T[P];
};

export type DeepMutable<T> = {
  -readonly [P in keyof T]: T[P] extends object
    ? T[P] extends Array<infer U>
      ? Array<DeepMutable<U>>
      : DeepMutable<T[P]>
    : T[P];
};

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

export type OmitByType<T, U> = {
  [P in keyof T as T[P] extends U ? never : P]: T[P];
};

export type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

export type PartialBy<T, K extends keyof T> = Omit<T, K> &
  Partial<Pick<T, K>>;

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

// Usage across your app
import { DeepPartial, DeepReadonly, Prettify } from './types/utils';
Exercise:
  1. Implement a DeepNonNullable<T> utility that removes null and undefined from all properties recursively
  2. Create a ValueOf<T> utility that extracts all possible value types from an object type
  3. Build a StrictOmit<T, K> that throws a compile error if K contains keys not in T (unlike built-in Omit)
  4. Implement PathValue<T, P> that returns the type at a specific path string (e.g., "user.profile.name")
  5. Create a PromisifyMethods<T> utility that wraps all method return types in Promises
  6. Build a comprehensive utility type library with at least 10 custom utilities and use them in a real-world data model

Summary

In this lesson, you explored advanced utility type patterns that extend TypeScript's type system to handle complex real-world scenarios. You learned to build DeepPartial, DeepReadonly, and other recursive utilities, create type-safe object path accessors with PathKeys, select properties by type, enforce "at least one" and "exactly one" property constraints, and flatten intersection types for better readability. These patterns empower you to create sophisticated, reusable type utilities that reduce boilerplate, enforce complex constraints, and make your codebase more maintainable and type-safe.