Skip to Content
Course content

224: Whiteboard Practice: Implementing DeepPartial Manually

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 Theme interface with the following structure:
    • colors: An object containing primary (string) and secondary (string).
    • spacing: An object containing small (number) and large (number).
    • breakpoints: An array of numbers (e.g., [320, 768, 1024]).
  • Create a constant called userCustomization with the type DeepPartial<Theme> that only overrides the primary color and the large spacing value.
// Your code here:
interface Theme {
  // ...
}

type DeepPartial<T> = {
  // ...
};

const userCustomization: DeepPartial<Theme> = {
  // ...
};
Rating
0 0

There are no comments for now.

to be the first to leave a comment.