TypeOfNaN

Deep Object Change Handlers in Typescript

Nick Scialli
May 22, 2020

post logo

Typescript’s ability to deeply-type objects is incredibly handy: it gives you confidence that you’re accessing the right keys on an object and that you’re using those keys as the right types. However, this typing doesn’t come for free: it can add complexity to things like change handlers. In this post, we’ll write a deep object change handler that both allows us to specify deep object types and satisfies the Typescript compiler.

A Sample Deep Object Type

Let’s use the following Settings type as an example. It contains some visual settings about our app and some information about our user.

type Settings = {
  display: {
    mode: 'light' | 'dark';
  };
  user: {
    name: string;
    age: number;
    admin: boolean;
  };
};

We can then create a sample object that satisfies this type. Let’s use the following example.

const settings: Settings = {
  display: {
    mode: 'dark',
  },
  user: {
    name: 'Betty',
    age: 27,
    admin: false,
  },
};

Writing a Change Handler

So what if we want a change handler that will change any property two levels deep in this object? The secret lies in the use of Generics. We can specify that our key is of type K, where K extends keyof Settings. Likewise, we can specify that our subkey is of type S, where S extends keyof Settings[K].

Putting this all together, we get the following change handler!

const updateSettings = <K extends keyof Settings, S extends keyof Settings[K]>(
  key: K,
  subkey: S,
  value: Settings[K][S]
): Settings => {
  const newSettings = {
    ...settings,
    [key]: {
      ...settings[key],
      [subkey]: value,
    },
  };

  return newSettings;
};

And there we have it: a framework by which we can update deep types and keep our Typescript compiler happy!

Nick Scialli

Nick Scialli is a senior UI engineer at Microsoft.

© 2024 Nick Scialli