TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
36: Recursive Conditional Types
You've likely already used the built-in Readonly<T> utility. It's great for a quick win when you want to ensure a prop or a config object doesn't get mutated. But as soon as your data structures get more than one level deep, you'll notice that Readonly is actually quite lazy—it's shallow. If you have a nested object, TypeScript will stop its protection at the first level, leaving your deep state wide open for mutations.
The illusion of immutability
Imagine we're building a complex state tree for a game engine. We have a GameState that contains a Player, and that player has a Position object. If we wrap the whole thing in Readonly, we feel safe, but we aren't.
type GameState = {
score: number;
player: {
name: string;
position: { x: number; y: number };
};
};
const state: Readonly<GameState> = {
score: 0,
player: {
name: "Hero",
position: { x: 10, y: 20 }
}
};
// This will error, as expected.
state.score = 10;
// This also errors.
state.player.name = "Villain";
// Wait... this actually works.
state.player.position.x = 50;
This is the "naive" way of handling immutability. We're applying a transformation to the top-level keys, but we're ignoring the types of the values those keys hold. Because Readonly doesn't recurse, state.player.position is still just a regular mutable object. In a large codebase, this is how bugs sneak in—you think you're working with an immutable snapshot, but a nested property is being changed by reference elsewhere.
Teaching the type to dig deeper
To fix this, we need the type to ask a question: "Is the value I'm currently looking at another object? If so, I should apply this same logic to that object too." This is where recursive conditional types come in. We aren't just mapping keys; we're conditionally triggering the type definition itself.
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
Look at that ternary operator inside the mapped type. I'm checking if T[P] (the value of the current property) extends object. If it does, the type "calls itself" by returning DeepReadonly<T[P]>. If it's a primitive—like a string or a number—it just returns the value as is. This creates a recursive loop that drills down until it hits the bottom of your data structure.
Where this approach breaks
Now, if we were reviewing this in a PR, I'd point out one major caveat: object in TypeScript is very broad. It includes arrays, dates, and regexes. If your state contains a Date object, DeepReadonly will try to map over the internal properties of the Date class, which is almost certainly not what you want. You'll end up with a weird, stripped-down version of a Date.
To handle this properly in a production environment, you'd want to refine your conditional. You might check if the type is specifically an array or if it's a known "leaf" object like a Date. For example, you could add a check like T[P] extends Date ? T[P] : ... to stop the recursion early. The trade-off is complexity; the more "special cases" you add to your recursive type, the harder it is for your teammates to reason about what the final output will actually be. But for most nested data trees, this recursive pattern is the only way to get true type-level safety.
📋 Practical Task
Building a DeepPartial for API Request Payloads
In many REST APIs, a PATCH request allows you to send only the fields you want to update. A standard Partial<T> makes all top-level properties optional, but it doesn't handle nested objects. If you send a partial update to a nested settings object, you don't want to be forced to provide every single setting just to change one flag.
Your Task: Create a recursive conditional type called DeepPartial<T>. This type should make every property of T optional, and if a property is an object, it should recursively make its properties optional as well.
Test your implementation against the following interface. Ensure that update can be assigned a value where only user.profile.bio is provided, without requiring user.profile.avatar or user.id.
interface UserProfile {
id: string;
profile: {
avatar: string;
bio: string;
socials: {
twitter: string;
github: string;
};
};
}
// Your type implementation here:
type DeepPartial<T> = {
// TODO: Implement recursive partial logic
};
const update: DeepPartial<UserProfile> = {
profile: {
bio: "Software Engineer"
// avatar and socials should be optional here
}
};
There are no comments for now.