Typescript Switches on Multiple Inputs

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}`);
}

(TypeScript Playground)

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.

Conversation
  • Chris says:

    I can’t even begin to tell you how wrong you’re using TS.

  • Join the conversation

    Your email address will not be published. Required fields are marked *