Writing Type-safe Defaults with TypeScript's Pick Type

A common pattern I use when writing TypeScript applications is creating default objects and overwriting some or all of those defaults with data provided by the user. But there's a common mistake when implementing this pattern that can be fixed with a little knowledge of TypeScript's built in utilities.

Implementation of the default pattern

Here's some code you might see in an app where ReadingListEntry objects are generated from a mix of defaults and user input, for example.

I like this pattern a lot, and it's especially good as the complexity of data in an application scales up. When objects have many fields or when users have multiple ways of creating data entries, centralizing defaults like this reduces a lot of boilerplate code (especially when combined with the Stage 4 object spread operator). And when things go wrong, it makes finding the culprit a lot easier.

But there's a problem with the way we're setting our defaults. To satisfy the TypeScript compiler, the default reading list entry's createdOn and id properties are set to unusable values. It may not seem like a big deal because we can see that those fields are always being overwritten with data from the user. But say we call generateReadingListEntry and forget to overwrite the default id value? The compiler wouldn't tell us about our mistake and our application would fail silently.

A mistake like this can and should be caught at compile time. Otherwise, this could end in a poor user experience at best or be catastrophic to the integrity of the user's data at worst.

'Pick' to the rescue

The best way to architect this pattern is to declare which properties of the ReadingListEntry object are safe to leave as default values and which ones we should always make sure to set manually when creating a ReadingListEntry object.

Fortunately, TypeScript has built in tools to make this possible. We can remove the id and createdOn properties of defaultReadingList and instead of annotating it as a full ReadingListEntry we can use TypeScript's Pick type to declare which properties of ReadingListEntry it has.

Now, if we use defaultReadingList to create ReadingListEntry objects, the TypeScript compiler will throw an error when we forget to manually set the missing properties.

We can even add a bit of expressiveness by giving the keys we're Picking a name:

In my experience with TypeScript's mapped types Pick has proven to be the most useful. It allows for creating type-safe partial slices of objects without needing to rely on TypeScript's inferencing or a complex naming strategy.