Skip to main content

TS Ep 50: DeepReadonly and DeepPartial

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 50: This Article
This is the crescendo of the Advanced module. To build a DeepReadonly or DeepPartial utility, we must combine Generics, Conditional Types, infer pattern matching, Mapped Types, and Recursion simultaneously.

1. The Problem with Readonly<T>
#

The built-in Readonly<T> and Partial<T> utilities provided by TypeScript are strictly shallow. They only operate on the top-level properties of an object.

interface Config {
  env: string;
  db: {
    host: string;
    port: number;
  }
}

const conf: Readonly<Config> = { 
  env: "dev", 
  db: { host: "localhost", port: 5432 } 
};

// 🟢 Top-level properties are frozen
// conf.env = "prod"; // Compiler Error!

// 🔴 Nested objects are NOT protected by Readonly<T>!
conf.db.port = 9999; 

If we want to pass this configuration object deeply down the component tree and guarantee that no one mutates the database port, we need a recursive mapping strategy.


2. Building DeepReadonly<T>
#

We need to build a utility type that iterates over the object and evaluates a logical condition: “If this property is an object, recursively run DeepReadonly on it. Otherwise, if it is a primitive, just return it.”

We will use Conditional Types (extends) to build a type-level router, and Mapped Types ([K in keyof T]) to rebuild the object structural layer.

type DeepReadonly<T> = 
  // 1. Primitive Router: If it's a primitive or function, do nothing.
  T extends Function | boolean | number | string | null | undefined 
    ? T
    
  // 2. Array Router: If it's an Array, use 'infer' to extract the element type,
  // wrap it in a ReadonlyArray, and recurse on the elements!
  : T extends Array<infer U> 
    ? ReadonlyArray<DeepReadonly<U>>
    
  // 3. Object Router: If it's a structural Object, map over its keys, 
  // add the 'readonly' modifier, and recurse on every value!
  : T extends object 
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    
  // 4. Fallback (e.g. for unusual types like unique symbols)
  : T;

Testing the Implementation
#

Let’s test our new utility on the deeply nested configuration object:

const deepConf: DeepReadonly<Config> = { 
  env: "dev", 
  db: { host: "localhost", port: 5432 } 
};

// 🟢 Both levels are now frozen!
// deepConf.env = "prod"; 
// deepConf.db.port = 9999; 

// ERROR: Cannot assign to 'port' because it is a read-only property.

3. Building DeepPartial<T>
#

Once you understand the architecture of DeepReadonly, building DeepPartial is trivial.

If you are updating a massive nested JSON configuration via a REST API PATCH request, you want every layer of the object to be completely optional.

type DeepPartial<T> = 
  // 1. Primitives
  T extends Function | boolean | number | string | null | undefined 
    ? T
    
  // 2. Arrays: Recurse into the array elements to make them optional
  : T extends Array<infer U> 
    ? Array<DeepPartial<U>>
    
  // 3. Objects: Add the '?' modifier, and recurse into values
  : T extends object 
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    
  // 4. Fallback
  : T;

Summary & Next Steps
#

In this episode, we combined every major concept covered in the Intermediate and Advanced modules:

  • Conditionals: Routing the type using extends.
  • Infer: Extracting array elements (infer U).
  • Mapped Types: Iterating over object keys ([K in keyof T]).
  • Modifiers: Adding readonly and ?.
  • Recursion: Types calling themselves to handle unknown depths.

If you understand how DeepReadonly is constructed, you understand 99% of advanced TypeScript metaprogramming. You are now equipped to read and understand almost any open-source library’s type definitions with confidence.

In Episode 51: Type-Level State Machines, we will explore how to model strict transition logic and impossible states to build bulletproof UIs!

typescript - This article is part of a series.
Part 50: This Article