Ramblings on Web Development and Software Architecture

Posted  a year ago


Conditionally making optional properties mandatory in typescript

A common case in applications is to work with types where certain properties are nullable. However, it is also common to need derived types of these same types which are stricter around nullablity of properties.

Let’s imagine a note taking app: We have a Notebook type where id is nullable because unsaved notebooks haven’t been assigned an id yet. However after saving it, we know for a fact that a Notebook instance will have an id and ideally we shouldn’t have to say notebook.id! in the rest of the application.

So, in this post we will explore a generic way to derive types like SavedNotebook where certain properties are conditionally mapped to non-optional.

TS comes with a NonNullable type, which essentially strips out null and undefined from any type.

We also have mapped types that allow us to map over a type and programmatically derive the type for each key.

So the following makes all values non-nullable:

Now, we can use conditional types to make only certain values non-nullable:

Or, a more generic implementation:

Our SavedNotebook type will effectively be:

Looks good! Are we done then ? It turns out, not quite.

Let’s tweak our Notebook interface a bit:

We made the properties optional. Our SavedNotebook type will no longer evaluate to what we expect:

What happened here ? Wasn’t our NonNullable type supposed to take care of both null and undefined ?

Well, it did, but it turns out typescript treats the following as different cases:

  1. Whether a key may (or may not) be present in an object type
  2. Whether the corresponding value may be undefined.

Mapped types preserve the former, and so our MandateProps is not able to remove the undefined from Notebook["id"].

So, what should be do if we do want to make that key mandatory as well ?

Thankfully typescript allows us to suffix our key with -? while mapping over types to remove optionality.

But doing that conditionally requires another workaround:

Omit<T, K> retains all the optional keys that we don’t care about and gets rid of the keys that are assignable to K. Then we create another type for the keys which match K, and while mapping over them we make the keys not optional. Intersection of the both gives us a type where all keys that are assignable to K are mandatory.

This is pretty much what we want. Omit is internally implemented using Pick and typescript is being lazy about Pick.