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

Top comments (0)

DEV runs on 100% open source code known as Forem.

Β 
Contribute to the codebase or host your own.
Β 
Check these out! πŸ‘‡