Type Safe and Exhaustive ‘switch’ statements, aka Pattern Matching in TypeScript

How to get TypeScript compiler to ascertain type safe and exhaustive matching in switch statement. Without using any external libraries.

Nayab Siddiqui
Technogise
5 min readJan 31, 2022

--

Photo by Mor THIAM on Unsplash

Motivation

Being a bit inclined towards functional programming, I wanted to try and build a “type safe” and “exhaustive” Pattern Matching in TypeScript, without using any additional libraries, and only using switch statements and the power of Type Inference and Control Flow Analysis provided by TS.

Preface

So... a switch statement…

While switch statements don’t need much introduction and are pretty ordinary in themselves, the sections in this blog would:

  • Introduce the code snippet (sample problem statement) we’ll be working with,
  • Shall discuss what are the limitations of switch statements when used in their naive form
  • And, then go on later to discuss a few enhancements (by harnessing native TypeScript features only!) that we can make to make our switch statements more Type-Safe and Exhaustive.

Note: The aim of this post is not to suggest the resulting code as a ‘better’ pattern for implementing calculator application or similar problems. It is merely intended for demonstrating the type safety and exhaustiveness that we can achieve while employing switch statements by harnessing only native (without usage of external libraries) features of TypeScript.

Problem Statement

We’ll work with a simple calculator application/function. To begin with, lets assume our calculator does only two operations, viz. Add and Multiply. A simple code for the same would be as follows:

Straight forward, isn’t it. We’ve got two types of Operations (Add and Multiply) and then a simple function using switch statement to perform the appropriate operation.

Now, what happens if we remove either of the case statements, or we add a new Operation type, say Subtract? TS compiler won’t complain in either scenarios because we have a default case as a fallback. We agree returning a 0(zero) from default is a big NO. Not only 0 is a legitimate result from one of the operations, it doesn’t signify that the calculator was not able to perform an unknown operation. We can also return an undefined or a null, or throw Error maybe from our default case. But all of these approaches introduce an extra branch and a possiblity that the client code will have to check for.

We can always write unit tests to make sure either of the above doesn’t go unnoticed.

But what if we can get the compiler to fail itself ?!?!

The ‘default’ case and ‘never’ type

Lets focus on default case. Let’s just log the Operation type that goes unmatched and is caught by the default case.

Notice that TypeScript infers operation inconsole.log statement as never type, as shown below.

This is because of control flow analysis done by TS. Since the operation type can be one of ‘ADD’ and ‘MULTIPLY’, there are naturally no more types left for it to assume in the default case, hence it assumes the never type.

Below are some excerpts from Marius Schulz’s blog on The never Type in TypeScript (highly recommended), and TypeScript docs which are most relevant to us in enhancing our solution further.

never is a subtype of and assignable to every type.

No type is a subtype of or assignable to never (except never itself). Additionally, it is a bottom-type.

In a function expression or arrow function with no return type annotation, if the function has no return statements, or only return statements with expressions of type never, and if the end point of the function is not reachable (as determined by control flow analysis), the inferred return type for the function is never.

Deriving from the above, lets create a function which will signal whether or not our switch case matching is exhaustive.

The function takes in a never type parameter and “never” returns. So in case we try to pass any other type as a parameter, TS compiler would be unhappy and fail. But since the return type is also never, it can be used in conjunction with any other return type within a function.

Stitching it all together

So lets replace the return 0 in our default case in our code with the exhaustiveMatchingGuard function we created in previous section.

Notice that TS compiler is happy with our code because (as elaborated in “The ‘default’ case and ‘never’ type” section) there is no other type left for operation to take on.

Let’s notice what happens if we add another type of Operation:

and Voila!! the compiler is not happy that we have forgotten to match the Subtract case!! It tells us that argument of type string is not assignable to type never , as expected!

And hints us that operation: ‘SUBTRACT’ is the culprit. Once we implement it, we’re back to our happy state!

Closing thoughts

One of the places that this kind of exhaustive matching comes very useful is while using redux and deriving next state based on different action types in reducers.

I hope this article gives some inspiration and inclination towards functional and type system side of TypeScript. I’m listing below some additional resources that you might find useful for futher explorations:

--

--

Nayab Siddiqui
Technogise

Engineering Leadership | Distributed Systems | Continuous Delivery | Books | Coffee. I write about Stories, Opinions and Musings around Tech & Culture