Skip to Content
Course content

38: Practice Exercise: Building a Deep Partial Utility Type

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

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);
Rating
0 0

There are no comments for now.

to be the first to leave a comment.