Produce more declarative TypeScript code with pattern matching
If you are not yet familiar with how type inference works in TypeScript, I would suggest you start with my previous article on the topic.
TypeScript’s static type checking is really powerful, and language features like type inference and type narrowing really make the language a joy to work with. But maybe you found yourself having to handle all of the possible types a variable could be, or wanting to narrow it to just a specific type, and you had to write a lot of code — often very imperative code.
Now there is nothing wrong with imperative code, but when working with some data structures or languages (e.g. JSX / React) it can be better to go declarative. This is where pattern matching shines!
What is pattern matching?
From the ts-pattern documentation:
Pattern Matching is a code-branching technique coming from functional programming languages, which lets you scrutinize the structure of values in a declarative way. It has proven itself to be less verbose and more powerful than imperative alternatives (if/else/switch statements), especially when branching on complex data structures or on several values.
Pattern Matching is implemented in Haskell, Rust, Swift, Elixir and many other languages.
What problem does pattern matching solve?
Let’s see a simple example:
This code has a few issues:
- Adding a new type to the union type of
networkStatus
will be handled like the'failure'
type. This code is not as “future-proof” as it could be. - This code has to be declared inside a function to be called inside JSX. You cannot use
if
statements inside JSX, only ternaries. - With a string as a data structure, it is pretty straightforward; but when you have to deal with more complex data structures or when you need to nest branches, it can become cumbersome.
How can I use pattern matching in TypeScript?
You may have guessed it, ts-pattern is an excellent library to introduce pattern matching in TypeScript. Let’s rewrite the previous example:
We just solved all of the problems we had with the previous version of the code.
Calling .exhaustive()
ensures we covered all of the possible types for networkStatus
, and if we add a 'timeout'
type to the union we get a comprehensive error:
It is also possible to call .run()
instead of .exhaustive()
to not check for exhaustiveness, or to call .otherwise(() => 'Something else happened')
to keep the behaviour we had with the last return
statement of our first example and have a fall-through case.
As the code doesn’t rely on if
statements, if can safely be called in JSX:
Also, ts-pattern works well with all data structures and I encourage you to check their documentation for advanced examples (e.g. the isMatching function).
Can ts-pattern only match literal values?
It can do much more than that! Let’s see a few other examples so you can see what is possible.
Type narrowing in branches:
Branching on primitive types:
Branching on values with .when()
:
There are lots of other features, check out the documentation!
Conclusion
ts-pattern is a nice addition to our tool kit and we use it extensively at Brigad to produce more readable, concise and declarative code!