-
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
38: Practice Exercise: Building a Deep Partial Utility Type
We've used Partial<T> a dozen times by now. It's great for simple update forms where you only want to send back a few changed fields. But as our data structures get more complex, I've noticed that Partial starts to feel... shallow. Let's look at why that's a problem and try to build a version that actually reaches the bottom of the tree.
The shallow limit
Imagine we're building a user settings page. Our configuration object is pretty nested because we want to group things logically. I've set it up like this:
interface UserPreferences {
theme: 'light' | 'dark';
notifications: {
email: boolean;
push: boolean;
marketing: boolean;
};
profile: {
username: string;
avatar: {
url: string;
size: number;
};
};
}
Now, I want to create a function that allows a user to update just their avatar size, without having to provide the rest of the profile or notifications objects. My first instinct is to use the built-in Partial:
type UpdatePreferences = Partial<UserPreferences>;
const update: UpdatePreferences = {
profile: {
avatar: {
size: 128
}
}
};
TypeScript immediately flags this. It's complaining that url is missing from the avatar object, and username is missing from profile. This is because Partial only makes the top-level keys optional. The profile key is optional, but if you do provide it, TypeScript expects the full profile object. That's not what I want.
Attempting a recursive map
I need this to be recursive. If a property is an object, I want to apply the "partial-ness" to that object too. Let's try to write a custom utility type for this. I'll call it DeepPartial.
My first thought is a mapped type that calls itself:
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
This looks elegant, but there's a catch. When I try to use this on our UserPreferences, TypeScript starts throwing errors. Why? Because not everything in UserPreferences is an object. For example, theme is a string. My type is trying to map over a string as if it were an object, and that's where it breaks.
Handling the primitives
I need a way to say: "If this property is an object, recurse. If it's a primitive (like a string or boolean), just leave it alone." This is where conditional types come in. I'll check if T extends an object.
Let's try this adjustment:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
I'll test it again with our update object from earlier. Now, the error on avatar.size disappears. The theme property is handled by the : T[P] branch, and the profile object is handled by the recursive DeepPartial<T[P]> branch. We're getting closer.
The array headache
Before I call this "done," I have to think about arrays. In TypeScript, an array [] is technically an object. If I have a list of tags in my preferences, my current DeepPartial will treat that array as an object and try to make its numeric indices optional. That's almost never what we actually want when we're talking about "partial updates."
To fix this, I'll refine the conditional check. I want to make sure I'm only recursing if the type is an object and not an array. I can use a check for Array or simply exclude it. Here is the refined version I usually end up with in my production code:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? U[] // Keep arrays as they are, or you could map them too
: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
By using infer U, I'm telling TypeScript: "If this is an array of something, just keep it as an array of that thing." Now we have a utility that can dive as deep as our data goes, without breaking on primitives or mangling our arrays. It's a lot more powerful than the standard Partial for complex state management.
📋 Practical Task
Exercise: Implementing DeepPartial for a Game Configuration
You are building a game engine where the GameConfig is a deeply nested object. You need to create a DeepPartial utility type so that the updateConfig function can accept a partial override of any setting, no matter how deep it is.
Requirements:
- Implement the
DeepPartial<T>type such that it recursively makes all properties optional. - Ensure that primitive types (string, number, boolean) are not processed recursively.
- Ensure that arrays are preserved as arrays and not treated as objects to be recursed.
interface GameConfig {
title: string;
video: {
resolution: {
width: number;
height: number;
};
fullscreen: boolean;
refreshRate: number;
};
audio: {
masterVolume: number;
musicVolume: number;
sfxVolume: number;
};
keyBinds: string[];
}
// TODO: Implement DeepPartial here
type DeepPartial<T> = // Your code here
function updateConfig(current: GameConfig, overrides: DeepPartial<GameConfig>) {
// Implementation details omitted
return { ...current, ...overrides };
}
// This should now compile without errors:
const myUpdate: DeepPartial<GameConfig> = {
video: {
resolution: {
width: 1920
}
},
keyBinds: ['W', 'A', 'S', 'D']
};
updateConfig({} as GameConfig, myUpdate);
There are no comments for now.