Article summary
I recently found myself wanting a two-dimensional switch statement in TypeScript. Here’s a way write one!
The Problem
TypeScript allows us to express unions of values:
type Direction = "up" | "down";
.. and we use this a lot.
In a recent situation, I had a couple of these at hand, and I wanted to attach behavior to each combination of them. This could be written with a chain of ifs.
function foo(color: "red" | "blue", num: "two" | "five") {
if (color === "red" && num === "two") {
// ...
} else if (color === "blue" && num === "five") {
// please make it stop
}
}
..yuck. Um, maybe nested switches?
switch (color) {
case "red":
switch (num) {
case "two":
// ...
break;
case "five":
// ...
break;
}
break;
case "blue":
switch (num) {
case "two":
// ...
break;
case "five":
// ...
break;
}
break;
}
That wasn’t any better.
I eventually found another way, but first, here’s a quick TypeScript refresher.
Some TypeScript Background
When you have two of these union types, they can be combined into new types:
These are called template literal types.
I’ve learned that this works with string initialization, too, given a const assertion:
See where this is going?
The 2D Switch
With the const string interpolation trick, we can write a conventional switch that covers the cross-product of two (or more) inputs:
type When = "before" | "after";
type What = "lunch" | "meeting";
function foo(when: When, what: What) {
const s = `${when}-${what}` as const;
switch (s) {
case "before-lunch":
return "grab wallet and keys";
case "before-meeting":
return "refill coffee";
case "after-meeting":
return "share synopsis";
case "after-lunch":
return "update backlog";
default:
assertUnreachable(s);
}
}
export function assertUnreachable(x: never): never {
// https://stackoverflow.com/questions/39419170/how-do-i-check-that-a-switch-block-is-exhaustive-in-typescript
throw new Error(`Unhandled case ${x}`);
}
Pros and Cons
I like this pattern for a few reasons:
- It has exhaustiveness checking. Expand the unions or forget a case, and you’ll get an editor squiggle. (See the “assertUnreachable” link above.)
- It’s dense and legible.
- You can reorder and combine cases.
On the other hand, one downside is that the switch could grow really big, really fast. (Thanks, Multiplication!)
A bigger problem is that, when you use the new interpolated type, you lose the narrowing of the underlying types. So I probably wouldn’t use this pattern if I needed to access other fields on a discriminated union:
If you need narrowing (or find yourself wanting to approximate other features of other languages’ pattern matching) then I’d check out ts-pattern.
I can’t even begin to tell you how wrong you’re using TS.
I like this simplification John!