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
readonlyand?. - 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!

