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
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 and could at worst be catastrophic to the integrity of the user's data.
'Partial' 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
Fortunately, TypeScript has built in tools to make this possible. We can remove the
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.