DEV Community

Hans Hoffman
Hans Hoffman

Posted on

Exhaustive pattern matching in TypeScript

The switch statements does not enforce exhaustive pattern matching (objective), the syntax is incredibly ugly (subjective) and is a poorly designed language feature (arguably objective). Thankfully, we can eliminate the shortcomings of the switch statement and improve the quality of our code using pattern matching instead.

There is a TC39 Stage 1 pattern matching proposal for the curious types here.


Example 1 (simple):

Say you have a string literal union type:

type Icon = "chart" | "file-check" | "paper-plane" | "users";
Enter fullscreen mode Exit fullscreen mode

and you want to conditionally render the appropriate icon so your first approach is the old trusted switch statement:

const renderIcon = (icon: Icon): JSX.Element => {
  switch (icon) {
    case "chart":
      return <ChartIcon />;
    case "file-check":
      return <FileCheckIcon />;
    case "paper-plane":
      return <PaperPlaneIcon />;
    case "users":
      return <UsersIcon />;
  }
};
Enter fullscreen mode Exit fullscreen mode

Here, the function's explicit return type is our only saving grace so to speak — not the switch statement itself. If we were to instead use the implicit return type of JSX.Element | undefined and then forget a case, the TypeScript compiler would not yell at us. I would therefore argue as a developer I have failed my current task as well as preventing those behind me from introducing bugs because I did communicate my intent clearly — this function needs to account for all possibilities, return a JSX.Element and have zero side effects.

We can refactor this using a library called ts-pattern to achieve that intent.

const renderIcon = (icon: Icon): JSX.Element => {
  return match(icon)
    .with("chart", () => <ChartIcon />)
    .with("file-check", () => <FileCheckIcon />)
    .with("paper-plane", () => <PaperPlaneIcon />)
    .with("users", () => <UsersIcon />)
    .exhaustive();
};
Enter fullscreen mode Exit fullscreen mode

Example 2 (advanced):

Say you have a branded type / discriminate union type / tagged union type:

type V1Report = {
  id: string;
  results: { /* whatever */ },
  year: string;
}

type V2Report = {
  id: string;
  results: { /* whatever */ },
  year: string;
}

type Report = { tag: "v1", value: V1Report } | { tag: "v2", value: V2Report }

// later in some function
match(report)
  .with({ tag: "v1" }, (v1Report) => /* do something with report */)
  .with({ tag: "v2" }, (v2Report) => /* do something with report */)
  .exhaustive()
Enter fullscreen mode Exit fullscreen mode

Oldest comments (0)