OmitTypes
One frequent annoyance that comes up in Typescript is redundant null checks. Take the following interface, which describes a standard JSONAPI resource:
A resource must have a type
, and may have attributes
and resources
. For example, we can define a User
with attribute name: string
and relationships to Post
s and Comment
s:
However, when working with resources that do not have attributes or relationships, our naive interface above is cumbersome to use, as our generic requires us to supply attributes
and relationships
even when they do not exist. Ideally, we want our generic to be flexible, while being ergonomic to use.
Approach 1: optional properties
One alternative would be to mark these as optional, which seems reasonable because as said above, not every resource has these properties.
However, this nullishness bleeds into our resources for which these properties exist:
This necessitates a redundant null check: the type system thinks that attributes
might not exist, though according to our schema, every User
should always have attribute
s. As a result, the type system is hindering rather than helping us.
Approach 2: default type arguments
Alternatively, we can set the trailing generic arguments to be set to null
:
This solves the previous problem of needing to pass types: if a resource lacks attributes or relationships, we simply pass nothing and our the type will default to null
. However, we now run into another hindrance when trying to create instances of a resource that do not have attributes
or relationships
:
Again, we are fighting with the type system: we are being asked to supply a value that we know via our scheme will never exist.
This can be bypassed by either explicitly passing relationships: null
(which is cumbersome), or by declaring my user as a Partial<ApiResource<...>>
, which loses type information for other properties we want to maintain as non-nullable. We could write a helper type MarkKeysAsOptional<T>
, which isn't a bad solution, but leaves us with two variants of the same type (User
and UserWithOnlyRequiredProperties
), which complicates our type system.
In a way, these two problems are opposites of each other. How can we construct our type in a way that fulfills both these use cases? More abstractly, how can we write a type constructor that omits "empty" keys from some other type?
Approach 3: OmitTypes
OmitTypes
We can accomplish this using mapped types and several intermediate types:
Using OmitTypes
, we can create type constructors that omit keys from interfaces based on the type of their values. We can now write a version of ApiResource
that handles both our cases:
If A
and R
are not passed, the resultant type will entirely omit the keys attributes
and relationships
. This is incredibly useful when our resources are modeled as tagged unions, as we never have to null check these properties if our types are properly defined. If we see that resource.type
corresponds to a resource which has attributes, the type system will know that resource.attributes
ALWAYS exists, and we don't have to check it. Conversely, when creating instances of a resource which doesn't have attributes or relationships, we don't have to specify these keys, as the type system knows these will NEVER exist.
Importantly, notice that our null
s are pushed to the absolute boundary of the type, where they do not "infect" our entire union of resources.
There are many other use cases for OmitTypes
. For example, one could collect all event handlers in an interface of React props by filtering out props that aren't functions matching a specific interface. A reverse of OmitTypes
can also be trivially constructed by replacing Pick
with Omit
. Below are some sample derived types:
If you know of other potentially useful use cases, please let me know! Or you can directly file a PR here.
I've since found that this (and other handy types) can be found in the library ts-essentials. Check it out!
Last updated