06 August 2019

Type checked "visitor" for discriminated unions using mapped types

Discriminated unions are one of the most useful features of TypeScript. After testing the discriminator value TypeScript can apply type checking based on the relevant member type of the union.

So, for a discriminated union of shapes, like this...

It's possible to write a calculateArea function using a switch on the discriminator (__typename in this case), like this...

Note that you can use the correct properties for the relevant member type of the union in each case body and you'll also get Intellisense prompting you with the correct property names.

Exhaustiveness problem

However, depending on the TypeScript compiler options you have set, you may end up missing cases where a type not covered by a case in the switch just falls through and the function returns an undefined. Adding the --noImplicitReturns options causes the compiler to complain if this is the case but this may not be an option for you if you make use of implicit returns. Another work around is to assign your instance to never in the switch's default e.g.

This will force the compiler warning but that is something you always need to remember to add when using a switch like this so it's not exactly a "pit of success".

Mapped types method

There is an alternative to the switch using mapped types instead which allows you to declare a map of the discriminator value to the correct operation for the type, like this...

The UnionMap mapped type which makes this work is defined like this...

It makes use of conditional types to enumerate the member types of the union in order to collate the discriminator values into a type e.g. UnionKeys<Shape, '__typename'> will be 'Circle' | 'Square' | 'Rectangle' | 'Triangle'. The mapped type UnionMap then requires a key for each of these string values to be present using [K in ...]: and it then does a "reverse lookup" of the original member type for that key using UnionPartForKey in order to provide the function argument type.

Using this method is cleaner and less error prone than the switch method. It will cause compile errors even with relaxed compiler options if a discriminator value is missed out of the map object.

Playground

I've created a TypeScript playground with mapped types here.

References

Written with StackEdit.