Bringing Pattern Matching to TypeScript 🎨 Introducing TS-Pattern
Tags: typescript, functional, declarative, react
Published: 2021-03-15 08:20:02

In the past few years, frontend development has become increasingly **declarative**. [React](https://reactjs.org/) shifted our mindsets from **imperatively** manipulating the DOM to **declaratively** expressing what the DOM should look like for a given state. It has been widely adopted by the industry, and now that we have realized how much easier it is to reason about declarative code and how many bugs are ruled out by embracing this paradigm, there is simply no going back.

It’s not only the user interface — state management libraries as well have been taking a declarative turn. Libraries such as [XState](https://github.com/davidkpiano/xstate), [Redux](https://redux.js.org/), and many more let you declaratively manage your application state to unlock the same benefits: writing code that is easier to **understand**, **modify** and **test**. Today, we truly live in a declarative programming world!

Yet, Javascript and TypeScript weren’t designed for this paradigm, and these languages are lacking a very important piece of the puzzle: **declarative code branching**.

Declarative programming essentially consists of defining **expressions** rather than **statements** — that is, code that evaluates to a value. The big idea is to separate the code **describing what needs to be done** from the code that **interprets** this description in order to produce **side effects**. For instance, making a React app essentially consists of describing how the DOM should look using JSX, and letting React mutate the DOM in a performant way under the hood.

## The problem with `if`, `else` and `switch`

If you have used React, you probably noticed that code branching inside JSX isn’t straightforward. The only way to use the `if`, `else` or `switch` statements we are used to is in self invoked functions (also called _Immediately Invoked Function Expressions_ or _IIFE_ for short):

```tsx
declare let fetchState:
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error" };

<div>
  {
    (() => {
      switch (fetchState.status) {
        case "loading":
          return <p>Loading...</p>;
        case "success":
          return <p>{fetchState.data}</p>;
        case "error":
          return <p>Oops, an error occured</p>;
      }
    })() // Immediately invoke the function
  }
</div>;
```

That's a lot of boilerplate and it doesn't look very nice. We can't blame React for this — it's just that imperative **statements** like `if`, `else` and `switch` (which do not return any value) do not fit well in a declarative context. We need **expressions** instead.

JavaScript does have a way to write code branching expressions: [ternaries](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator). But they have several limitations...

## Ternaries are not enough

Ternaries are a concise way of returning two different values based on a boolean:

```ts
bool ? valueIfTrue : valueIfFalse;
```

The simple fact that ternaries are expressions makes them the _de facto_ way of writing code branches in React. Here's what most of our components look like nowadays:

```tsx
const SomeComponent = ({ fetchState }: Props) => (
  <div>
    {fetchState.status === "loading" ? (
      <p>Loading...</p>
    ) : fetchState.status === "success" ? (
      <p>{fetchState.data}</p>
    ) : fetchState.status === "error" ? (
      <p>Oops, an error occured</p>
    ) : null}
  </div>
);
```

**Nested ternaries**. They are a bit hard to read, but we just don't have any better option. What if we want to define and reuse a variable inside one of our branches? This seems pretty basic, but there is no straightforward way to do that with ternaries. What if we don’t want a default case and we just want to make sure we're handling all possible cases? This is called **exhaustiveness checking**, and guess what: we can’t do that with ternaries either.

## The _status quo_ of exhaustiveness checking

There are workarounds to make TypeScript check that a switch statement is exhaustive. One of them is to call a function that takes a parameter with the `never` type:

```tsx
// This function is just a way to tell TypeScript that this code
// should never be executed.
function safeGuard(arg: never) {}

switch (fetchState.status) {
  case "loading":
    return <p>Loading...</p>;
  case "success":
    return <p>{fetchState.data}</p>;
  case "error":
    return <p>Oops, an error occured</p>;
  default:
    safeGuard(fetchState.status);
}
```

This will only type-check if `status` has type `never`, which means that all possible cases are handled. This looks like a good solution, but if we want to do that in our JSX, we are back to an _IIFE_:

```tsx
<div>
  {(() => {
    switch (fetchState.status) {
      case "loading":
        return <p>Loading...</p>;
      case "success":
        return <p>{fetchState.data}</p>;
      case "error":
        return <p>Oops, an error occured</p>;
      default:
        safeGuard(fetchState.status);
    }
  })()}
</div>
```

Even more boilerplate.

What if we want to branch based on two values instead of one? Let's say we want to write a state reducer. [It's considered good practice](https://dev.to/davidkpiano/you-don-t-need-a-library-for-state-machines-k7h) to branch both on the current state and on the action to prevent invalid state changes. The only option we have to ensure we are handling every case is to nest several switch statements:

```ts
type State =
  | { status: "idle" }
  | { status: "loading"; startTime: number }
  | { status: "success"; data: string }
  | { status: "error"; error: Error };

type Action =
  | { type: "fetch" }
  | { type: "success"; data: string }
  | { type: "error"; error: Error }
  | { type: "cancel" };

const reducer = (state: State, action: Action): State => {
  switch (state.status) {
    case "loading": {
      switch (action.type) {
        case "success": {
          return {
            status: "success",
            data: action.data,
          };
        }
        case "error": {
          return {
            status: "error",
            error: action.error,
          };
        }
        case "cancel": {
          // only cancel if the request was sent less than 2 sec ago.
          if (state.startTime + 2000 < Date.now()) {
            return {
              status: "idle",
            };
          } else {
            return state;
          }
        }
        default: {
          return state;
        }
      }
    }
    default:
      switch (action.type) {
        case "fetch": {
          return {
            status: "loading",
            startTime: Date.now(),
          };
        }
        default: {
          return state;
        }
      }

      safeGuard(state.status);
      safeGuard(action.type);
  }
};
```

Even though this is safer, it’s a lot of code and it's very tempting to go for the shorter, unsafe alternative: only switching on the action.

## _There must be a better way to do this?_

Of course there is. Once more, we need to turn our gaze to functional programming languages, and see how they have been doing it all this time: **Pattern Matching**.

Pattern matching is a feature implemented in many languages like Haskell, OCaml, Erlang, Rust, Swift, Elixir, Rescript… The list goes on. There is even a [TC39 proposal from 2017](https://github.com/tc39/proposal-pattern-matching) to add pattern matching to the EcmaScript specification (defining the JavaScript syntax and semantic). The proposed syntax looks like this:

```tsx
// Experimental EcmaScript pattern matching syntax (as of March 2023)
match (fetchState) {
  when ({ status: "loading" }): <p>Loading...</p>
  when ({ status: "success", data }): <p>{data}</p>
  when ({ status: "error" }): <p>Oops, an error occured</p>
}
```

The pattern matching expression starts with the `match` keyword followed by the value we want to branch on. Each code branch starts with a `when` keyword followed by the pattern: **the shape** our value **must match** for this branch to be executed. If you know about [destructuring assignements](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) this should feel pretty familiar.

Here is how the previous reducer example would look with the proposal:

```ts
// Experimental EcmaScript pattern matching syntax (as of March 2023)
const reducer = (state: State, action: Action): State => {
  return match ([state, action]) {
    when ([{ status: 'loading' }, { type: 'success', data }]): ({
      status: 'success',
      data,
    })
    when ([{ status: 'loading' }, { type: 'error', error }]): ({
      status: 'error',
      error,
    })
    when ([state, { type: 'fetch' }])
    if (state.status !== 'loading'): ({
      status: 'loading',
      startTime: Date.now(),
    })
    when ([{ status: 'loading', startTime }, { type: 'cancel' }])
    if (startTime + 2000 < Date.now()): ({
      status: 'idle',
    })
    when (_): state
  }
};
```

So much better!

I didn't run any scientific study on this, but I _believe_ that pattern matching takes advantage of our brain's natural ability for **pattern recognition**. A pattern looks like the shape of the value we want to match on, which makes the code much easier to read than a bunch of `if`s and `else`s. It's also shorter and, most importantly, it's an **expression**!

I’m very excited about this proposal, but it’s still in stage 1 and it is unlikely to be implemented for at least several years (if ever).

---

# Bringing Pattern matching to TypeScript

A year ago, I started working on what was then an experimental library implementing pattern matching for TypeScript: [ts-pattern](https://github.com/gvergnaud/ts-pattern). At first, I didn’t expect that it would be possible to implement in userland something even close to native language support in terms of **usability** and **type safety**. It turns out I was wrong. After several months of work I realized that TypeScript’s type system was powerful enough to implement a pattern matching library with all the bells and whistles **we can expect from native language support**.

Today, I’m releasing the **[version 3.0 of ts-pattern](https://github.com/gvergnaud/ts-pattern/releases)** 🥳🎉✨

Here is **the same reducer** written with **ts-pattern**:

```ts
import { match, P } from 'ts-pattern';

const reducer = (state: State, action: Action) =>
  match<[State, Action], State>([state, action])
    .with([{ status: 'loading' }, { type: 'success', data: P.select() }], data => ({
      status: 'success',
      data,
    }))
    .with([{ status: 'loading' }, { type: 'error', error: P.select() }], error => ({
      status: 'error',
      error,
    }))
    .with([{ status: P.not('loading') }, { type: 'fetch' }], () => ({
      status: 'loading',
      startTime: Date.now(),
    }))
    .with([{ status: 'loading', startTime: P.when(t => t + 2000 < Date.now()) }, { type: 'fetch' }], () => ({
      status: 'idle',
    }))
    .with(P._, () => state) // `P._` is the catch-all pattern.
    .exhaustive();

`
```

### Perfectly fits in a declarative context

`ts-pattern` works in any (TypeScript) environment and with any framework or technology. Here is the React component example from earlier:

```tsx
declare let fetchState:
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error" };

<div>
  {match(fetchState)
    .with({ status: "loading" }, () => <p>Loading...</p>)
    .with({ status: "success" }, ({ data }) => <p>{data}</p>)
    .with({ status: "error" }, () => <p>Oops, an error occured</p>)
    .exhaustive()}
</div>;
```

No need for an _IIFE_, a `safeGuard` function or nested ternaries. It fits right in your JSX.

### Compatible with any data structure

Patterns can be anything: objects, arrays, tuples, Maps, Sets, **nested in any possible way**:

```ts
declare let x: unknown;

const output = match(x)
  // Literals
  .with(1, (x) => ...)
  .with("hello", (x) => ...)
  // Supports passing several patterns:
  .with(null, undefined, (x) => ...)
  // Objects
  .with({ x: 10, y: 10 }, (x) => ...)
  .with({ position: { x: 0, y: 0 } }, (x) => ...)
  // Arrays
  .with(P.array({ firstName: P.string }), (x) => ...)
  // Tuples
  .with([1, 2, 3], (x) => ...)
  // Maps
  .with(new Map([["key", "value"]]), (x) => ...)
  // Set
  .with(new Set(["a"]), (x) => ...)
  // Mixed & nested
  .with(
    [
      { type: "user", firstName: "Gabriel" },
      { type: "post", name: "Hello World", tags: ["typescript"] }
    ],
    (x) => ...)
   // This is equivalent to `.with(__, () => …).exhaustive();`
  .otherwise(() => ...)

```

In addition, the type system will reject any pattern that doesn't match the input type!

### Built with type safety and type inference in mind

For every `.with(pattern, handler)` clause, the input value is piped to the `handler` function with a type narrowed down to what the `pattern` matches.

```ts
type Action =
  | { type: "fetch" }
  | { type: "success"; data: string }
  | { type: "error"; error: Error }
  | { type: "cancel" };

match<Action>(action)
  .with({ type: "success" }, (matchedAction) => {
    /* matchedAction: { type: 'success'; data: string } */
  })
  .with({ type: "error" }, (matchedAction) => {
    /* matchedAction: { type: 'error'; error: Error } */
  })
  .otherwise(() => {
    /* ... */
  });
```

### Exhaustiveness checking support

`ts-pattern` nudges you towards safer code by making exhaustive matching the default:

```ts
type Action =
  | { type: 'fetch' }
  | { type: 'success'; data: string }
  | { type: 'error'; error: Error }
  | { type: 'cancel' };

return match(action)
  .with({ type: 'fetch' }, () => /* ... */)
  .with({ type: 'success' }, () => /* ... */)
  .with({ type: 'error' }, () => /* ... */)
  .with({ type: 'cancel' }, () => /* ... */)
  .exhaustive(); // This compiles

return match(action)
  .with({ type: 'fetch' }, () => /* ... */)
  .with({ type: 'success' }, () => /* ... */)
  .with({ type: 'error' }, () => /* ... */)
  // This doesn't compile!
  // It throws a `NonExhaustiveError<{ type: 'cancel' }>` compilation error.
  .exhaustive();
```

You can still opt-out by using `.run()` instead of `.exhaustive()` if you really need to:

```ts
return match(action)
  .with({ type: 'fetch' }, () => /* ... */)
  .with({ type: 'success' }, () => /* ... */)
  .with({ type: 'error' }, () => /* ... */)
  .run(); // ⚠️ This is unsafe but it compiles
```

### Wildcards

If you need a pattern to always match, you can use the `P._` (wildcard) pattern. This is a pattern that matches anything:

```ts
import { match, P } from 'ts-pattern';

match([state, event])
  .with(P._, () => state)
  // You can also use it inside another pattern:
  .with([P._, { type: 'success' }], ([_, event]) => /* event: { type: 'success', data: string } */)
  // at any level:
  .with([P._, { type: P._ }], () => state)
  .exhaustive();
```

It's also possible to match a **specific type** of input with `P.string`, `P.boolean` and `P.number`. It's especially useful when dealing with `unknown` values, maybe coming from an API endpoint:

```ts
import { match, P } from "ts-pattern";

type Option<T> = { kind: "some"; value: T } | { kind: "none" };
type User = { firstName: string; age: number; isNice: boolean };

declare let apiResponse: unknown;

const maybeUser = match<unknown, Option<User>>(apiResponse)
  .with({ firstName: P.string, age: P.number, isNice: P.boolean }, (user) =>
    /* user: { firstName: string, age: number, isNice: boolean } */
    ({ kind: "some", value: user })
  )
  .otherwise(() => ({ kind: "none" }));

// maybeUser: Option<User>
```

### When clauses

You can use the `when` helper function to make sure the input respects a guard function:

```ts
import { match, P } from 'ts-pattern';

const isOdd = (x: number) => Boolean(x % 2)

match({ x: 2 })
  .with({ x: P.when(isOdd) }, ({ x }) => /* `x` is odd */)
  .with(P._, ({ x }) => /* `x` is even */)
  .exhaustive();
```

You can also call `.with()` with a guard function as second parameter:

```ts
declare let input: number | string;

match(input)
  .with(P.number, isOdd, (x) => /* `x` is an odd number */)
  .with(P.string, (x) => /* `x` is a string */)
  // Doesn't compile! the even number case is missing.
  .exhaustive();
```

Or just use `.when()`:

```ts
match(input)
  .when(isOdd, (x) => /* ... */)
  .otherwise(() => /* ... */);
```

### Property selection

When matching on a deeply nested input, it's often nice to extract pieces of the input to use in the handlers and avoid having to separately destructure the input. The `select` helper function enables you to do that:

```tsx
import { match, select } from "ts-pattern";

type input =
  | { type: "text"; content: string }
  | { type: "video"; content: { src: string; type: string } };

match(input)
  // Anonymous selections are directly passed as first parameter:
  .with(
    { type: "text", content: P.select() },
    (content) => <p>{content}</p> /* content: string */
  )
  // Named selections are passed in a `selections` object:
  .with(
    { type: "video", content: { src: P.select("src"), type: P.select("type") } },
    ({ src, type }) => (
      <video>
        <source src={src} type={type} />
      </video>
    )
  )
  .exhaustive();
```

### Tiny

Since this library is mostly type-level code, it has a **tiny bundle footprint**: **only 1.6kB** [once minified and gzipped](https://bundlephobia.com/result?p=ts-pattern@3.0.2)!

### Drawbacks

For the type inference and exhaustiveness checking to work properly, `ts-pattern` relies on type level computations that might slow down the typechecking of your project. I tried (and will continue to try) to make it as fast as possible, but it will always be slower than a `switch` statement. Using `ts-pattern`, means trading some compilation time for type safety and for code that is easier to maintain. If this trade-off doesn't appeal to you, that's ok! You don't have to use it!

### Installation

You can install it from [npm](https://www.npmjs.com/package/ts-pattern)

```
npm install ts-pattern
```

Or [yarn](https://yarnpkg.com/)

```
yarn add ts-pattern
```

## Conclusion

I love tools which make it easy to write better code. I was heavily inspired by [ImmutableJS](https://github.com/immutable-js/immutable-js) and [Immer](https://github.com/immerjs/immer) in that regard. Simply by providing a nicer API to manipulate immutable data structures, these libraries greatly encouraged the adoption of immutability in the industry.

Pattern matching is great because it nudges us towards writing code that is safer and more readable, and `ts-pattern` is my humble attempt to popularise this concept in the TypeScript community. **ts-pattern v3.0** is the **first LTS version**. Now that the technical challenges are solved, this version focuses on performance and usability. I hope you will enjoy it.

---

**✨ [Star it on GitHub](https://github.com/gvergnaud/ts-pattern) ✨ if you think it’s exciting!**

---

You can find the **full API reference** on [the ts-pattern repository](https://github.com/gvergnaud/ts-pattern)

---

👉 [I posted the link on Hacker News](https://news.ycombinator.com/item?id=26463828#26463975) don't hesitate to post a comment in the thread if you have any question, I'll try to answer to everyone!

---

##### PS: Shouldn't we just switch to languages supporting pattern matching?

Some languages like [Rescript](https://rescript-lang.org/) support pattern-matching and compile to JS. If I were to start a new project, I personally would love to try them! We don't always have the luxury of starting a new project from scratch though, and the TypeScript code we write could benefit a lot from adopting pattern matching. Mine certainly would. I hope you found my case convincing 😉

##### PPS: Inspiration

This library was heavily inspired by the great article [Pattern Matching in TypeScript with Record and Wildcard Patterns](https://medium.com/swlh/pattern-matching-in-typescript-with-record-and-wildcard-patterns-6097dd4e471d) by Wim Jongeneel. Read it if you want to have a rough idea of how ts-pattern works under the hood.

👋 Cya!


[Update April 2023]: Update examples to use TS-Pattern v4 instead of v3.