-
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
224: Whiteboard Practice: Implementing DeepPartial Manually
We've all been there: you're building a complex state update or a configuration override, and you reach for the built-in Partial<T> utility. It works great for the top level, but the moment you have a nested object, TypeScript starts yelling at you because the inner properties are still required. Today, we're going to build DeepPartial from scratch. I want us to treat this like a whiteboard session—we'll start with a naive implementation and refine it as we hit walls.
The "Shallow" Problem
Let's start with a real-world scenario. Imagine we have a game state for a strategy game. It's deeply nested because, well, that's how game states usually are.
interface GameState {
user: {
id: string;
profile: {
username: string;
avatarUrl: string;
};
};
settings: {
audio: {
masterVolume: number;
musicVolume: number;
};
graphics: {
resolution: string;
fpsLimit: number;
};
};
}
Now, imagine I want a function that allows me to update any part of this state without providing the whole object. I'll try the standard Partial:
const updateState = (state: GameState, patch: Partial<GameState>) => {
// implementation logic
};
// This works fine
updateState(currentState, { settings: { audio: { masterVolume: 0.5 } } });
// ERROR: Property 'musicVolume' is missing in type '{ masterVolume: number; }'
// but required in type 'GameState["settings"]["audio"]'.
See the problem? Partial only goes one level deep. It made settings optional, but it didn't make settings.audio optional. It just expects the full audio object if you choose to provide the settings key.
The First Naive Attempt
I'll try to fix this by making the mapped type recursive. If I'm mapping over the keys, why not just call the type itself on the value?
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
};
At first glance, this looks elegant. I'm telling TypeScript: "For every property P in T, make it optional, and then make its value a DeepPartial of whatever that value was."
But if I try to use this on a simple string or number, things go sideways. If T[P] is a string, the type tries to map over the properties of a string (like length, charAt, etc.). This is a disaster. We only want to recurse if the value is actually an object we can dive into.
Adding the Guardrail
We need a way to say: "If this is an object, recurse. If it's a primitive, just leave it alone." This is where conditional types come in. I'll use a check to see if the property is an object.
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
I'm feeling better about this. Now, if T[P] is a string, it hits the false branch of the ternary and just returns T[P]. If it's an object, it dives deeper. Let's test it against our GameState again.
const patch: DeepPartial<GameState> = {
settings: {
audio: {
masterVolume: 0.8
}
}
};
This actually works! TypeScript is happy. But wait—there's a catch. What happens if T[P] is an array? In JavaScript, arrays are objects. My current logic will treat an array as an object and try to map over its numeric indices and prototype methods (like push and pop), which is almost certainly not what we want for a partial update.
Handling the Array Edge Case
To truly make this production-ready, we need to ensure arrays are treated as values rather than objects to be recursed into. I'll add a check for arrays specifically.
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<any>
? T[P]
: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
I've just created a hierarchy of checks. First, is it an array? If so, stop. Is it an object? If so, recurse. Otherwise, it's a primitive; stop. This is the robust way to handle deep partials. It gives you the flexibility to update a single nested field deep within your state tree without the compiler demanding every single sibling property be present.
📋 Practical Task
Implementing a DeepPartial Configuration Override
You are building a Theme engine for a UI library. The theme object is deeply nested to allow for specific component overrides. Your task is to implement the DeepPartial type we discussed and use it to create a "User Override" object.
Requirements:
- Implement the
DeepPartial<T>type such that it recursively makes all properties optional, but handles arrays and primitives correctly. - Define a
Themeinterface with the following structure:colors: An object containingprimary(string) andsecondary(string).spacing: An object containingsmall(number) andlarge(number).breakpoints: An array of numbers (e.g.,[320, 768, 1024]).
- Create a constant called
userCustomizationwith the typeDeepPartial<Theme>that only overrides theprimarycolor and thelargespacing value.
// Your code here:
interface Theme {
// ...
}
type DeepPartial<T> = {
// ...
};
const userCustomization: DeepPartial<Theme> = {
// ...
};There are no comments for now.