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 Pick
ing 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.