For frontend development at Swan, we use TypeScript, a typed superset of JavaScript that, in our opinion, needs no introduction.
The downside of using TypeScript is that it deprives us of several paradigms that are absent in JS and that we may be accustomed to using when we worked with other languages in the past. One of the biggest shortcomings for us is the lack of pattern matching (it's currently being considered, but still in draft).
Pattern…what? And what does it solve?
Yes, pattern matching.
Let's imagine that we want to describe the loading state of a request, which can be uninitialized, in progress, successfully completed, or completed with an error.
Naively, we could write it as follows:
As you can see, this code is hard to read and maintain, and is also prone to errors: what happens if a setUser((prevState) => ({ ...prevState, user: newRandomUser })) sneaks into your code? You could end up with the user state similar to { isLoading: false, data: newRandomUser, error: previousUserError }, an impossible case for our UI.
Let’s try discriminated unions
Fortunately, TypeScript offers us a solution to this problem with discriminated unions: unions of object types with a common property (the discriminant) allowing us to determine which kind of shape our value has.
Let's refactor our previous code using discriminated unions:
Aaaah, much better already!
We’re close, but not there yet
However, a few problems still remain:
- Since switch is not an expression (it doesn't return a value), we need to wrap each condition in an IIFE (an alternative would be to continue using nested ternaries).
- We need to introduce a new helper function, exhaustive, to ensure that we handle all cases properly (this way, TypeScript will warn us if the union evolve over time and we miss a new case).
- It works for simple cases, but if we want to add another condition, let's say, to display something else if the user is disabled, the condition code would become nested again.
Introducing ts-pattern: handling complex conditions as an expression
ts-pattern is a pattern-matching library for TypeScript developed by Gabriel Vergnaud. It allows you to compare an input value against different patterns, execute a function if it matches, and return the result. Consider this example:
As you can see:
- match() is an expression; it returns a value. There’s no need to wrap it in an IIFE.
- Using .exhaustive() removes the need for our custom exhaustive helper function.
- match() can also solve the “another condition” problem elegantly by adding a new branch:
Pattern matching and GraphQL, a match() made in heaven
Swan exposes a GraphQL API, so it’s quite easy to leverage pattern matching to consume our API, and it’s not only for the frontend!
Let's start by generating an SDK using GraphQL Code Generator and write a GraphQL operation:
The generated return type is a discriminated union, similar to the one we wrote before:
So we can consume it like this:
Since the matching is exhaustive, you will be notified of any new rejections added in the API updates and you will have to update your code to handle them accordingly. If you wish to ignore this, you can use .otherwise() instead:
Of course, we are only scratching the surface of what is possible. To delve deeper, you can visit the documentation of ts-pattern, explore our frontend codebase (where we even use pattern matching to handle results returned by our router) or clone and experiment with the latest example repository.
Have fun!