Making TypeScript's Partial type work for nested objects
TL;DR?
If you’re just here to copy the type definition, jump to the conclusion and copy away. β
Without further ado…
Meet TypeScript’s Partial type
There are functions in which you do not want to pass or accept the full object, but rather a subset of its properties. An example would be a database update function.
Consider this fictional User
type:
interface User {
id: number;
firstName: string;
lastName: string;
}
We could write the update function like this:
function updateUser(data: User) {
// Update here.
}
But this will require us to always pass a full User
object.
We would rather be able to just pass the attributes of the user that changed, like this:
updateUser({ lastName: "Johnson" });
However, TypeScript won’t accept this, because this is not a User
, it’s an arbitrary object with a lastName
property! Of course we can escape using the any
type, but that won’t give us any of TypeScript’s benefits.
Luckily, TypeScript provides so-called Utility Types, one of which is the Partial
type.
We can use it to fix our updateUser
function:
function updateUser(data: Partial<User>) {
// Update here.
}
Awesome! This works: the function will accept an object consisting of some or all of User
’s properties.
π Read the TypeScript documentation on the Partial type
Complicating things with nested objects
All right, now let’s turn up the heat on poor Partial
.
Let’s say our User
type contains nested objects:
interface User {
id: number;
firstName: string;
lastName: string;
address: {
street: string;
zipcode: string;
city: string;
};
}
Our updateUser
function will still work, and you can definitely omit the address
property, but what you can’t do, is pass a partial address
object. This will fail:
updateUser({
address: {
city: "Amsterdam",
},
});
TypeScript will yell at you:
Type '{ city: string; }' is missing the following properties
from type '{ street: string; zipcode: string; city: string; }':
street, zipcode
From this we can conclude that Partial
allows you to omit any property from the original interface, but you cannot change the shape of the values. Any nested object should be in the original shape, and thus contain all of its properties.
We can fix this by re-creating the Partial
type.
Let’s take a look at the Partial
type’s definition:
type Partial<T> = { [P in keyof T]?: T[P] };
What’s going on here?
keyof
is a so-called type operator. It produces a union type of all the keys of an object. For example:
type Point = { x: number; y: number };
type P = keyof Point;
P
in this example is equivalent to type P = "x" | "y"
.
π Read the TypeScript manual on the keyof type operator
Another relevant keyword in the Partial
definition is in
. It can be used to define a Mapped Type.
type Point = { x: number; y: number };
type P = {
[Property in keyof Point]: {
value: number;
units: "px" | "em" | "rem";
};
};
In this example P
is a type that supports objects looking like this:
const foo: P = {
x: { value: 42, units: "px" },
y: { value: 999, units: "rem" },
};
π Read the TypeScript manual on Mapped Types
Last but not least, the little question mark in the Partial
type definition might have escaped your attention but is actually the most important part of the definition. It makes properties optional, which is the raison d’Γͺtre of the Partial
type!
Now that we have a solid understanding of the definition, we can see clearly that nothing in this type would allow partial nested objects. Let’s come up with our own type to fix this. We will call it Subset
since Partial
is taken.
type Subset<K> = {
[attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr];
};
This looks a little daunting, but look closely, and you’ll see that most of it is equivalent to the original Partial
type.
[attr in keyof K]?:
This part again means: create a type containing all properties of K
, and make all of them optional.
The definition of the value is a little more convoluted:
K[attr] extends object ? Subset<K[attr]> : K[attr];
This value takes the form of a JavaScript ternary operator. TypeScript allows dynamic structures like this thanks to a feature called Conditional Types. Conditional Types allow you to inspect a given parameter and create a logic branch in the definition of your type.
Here’s an example to illustrate this behavior:
interface Point {
x: number;
y: number;
}
type Point3D<P> = P extends Point
? {
[key in keyof P]: P[key];
} & {
z: number;
}
: never;
When given a Point
, this Point3D
generic type will take all properties from the given type P
, and add a z
property. But given any other type, it’s defined as never
.
π Read the TypeScript manual on Conditional Types
With this, we can take another look at the property values of our Subset
type:
[attr in keyof K]?: K[attr] extends object ? Subset<K[attr]> : K[attr];
This says: for every property of type K
, see whether its value extends object
, and if so, make it also a Subset
, otherwise just copy its definition from the original.
And with that, we can update our updateUser
function to be:
function updateUser(data: Subset<User>) {
// Update here.
}
Now the function will allow any partial User
object, and even respect partial nested objects. Great!
Update October 2022
One more optimization came to us from reader Michael Dahm on Twitter. Michael noticed that our implementation does not allow for properties to be either an object or null/undefined in the partial implementation.
He suggests this format:
type Subset<K> = {
[attr in keyof K]?: K[attr] extends object
? Subset<K[attr]>
: K[attr] extends object | null
? Subset<K[attr]> | null
: K[attr] extends object | null | undefined
? Subset<K[attr]> | null | undefined
: K[attr];
};
This fixes that problem and with that, is a more robust implementation altogether. Thanks Michael!
In conclusion
type Subset<K> = {
[attr in keyof K]?: K[attr] extends object
? Subset<K[attr]>
: K[attr] extends object | null
? Subset<K[attr]> | null
: K[attr] extends object | null | undefined
? Subset<K[attr]> | null | undefined
: K[attr];
};
This type probably looks very complicated to beginning TypeScript developers, but when broken down, the parts refer to well-documented behaviors and concepts.
TypeScript is an amazingly expressive language, which enables you to come up with highly dynamic interfaces that perfectly adhere to the rules of your application.
The profit, in the end, is in your editor, which will tell you exactly what’s expected and help you do the right thing.