DEV Community

Titouan CREACH
Titouan CREACH

Posted on

Why is pattern matching so great

Ok, let's start with a story. You are a front-end developper and you work with your mate Bob. Bob is a very nice guy but not that rigorous. He sometimes makes breaking changes, return null instead of the response you expected. But you know, life is not perfect.

Anyway, Bob and you are asked to show a profile page for a user.

So let's start to design the response:

{
  firstname: "John",
  lastname: "Doe",
  dob: "1990-03-11T00:00:00Z"
  address: {
    location: [48.864716, 2.349014],
    street: "43 Avenue",
  }
}

Enter fullscreen mode Exit fullscreen mode

So after one or two coffee, you decide to write this typescript type:

export type UserProfile = {
  firstname: string;
  lastname: string;
  dob: string;
  address: {
    location: [number, number];
    street: string;
  };
};
Enter fullscreen mode Exit fullscreen mode

Then, you come up with something like:

export function UserProfile({ profile }: { profile: UserProfile }) {
  return (
    <h1> Hello { profile.firstname } </h1>
    <Map lat={profile.address.location[0]} lng={profile.address.location[1]} />
  );
}
Enter fullscreen mode Exit fullscreen mode

After few minutes you understand that a user may not have filled his profile. So let's check if location exist. Make it optional in typescript first:

export type UserProfile = {
  ...
  address: {
    location?: [number, number]  
  }
}
Enter fullscreen mode Exit fullscreen mode

After the check, you see that Bob decided to, sometimes, returns "NaN" instead of a valid location because you know: typeof NaN === "number".

So now you don't trust Bob anymore and you decide to check everything.

In english you would say:

In order to display a map, I need profile to be an object, and then, check if a key address exist and the value is an object, in that object, I want a key: position to exist. I want the value to be an array of 2 elements. I want these two element to be number not "Nan".

So, basically (I know shorter syntaxes exist), if I want to express that I should write:

if (
  profile && profile["address"] && profile["address"]["location"] &&  Array.isArray(profile["address"]["location"]) && profile["address"]["location"].length === 2 && typeof profile["address"]["location"][0] === "number" && typeof profile["address"]["location"][1] === "number" && !isNaN(profile["address"]["location"][0]) && !isNaN(profile["address"]["location"][1])) {
  // display the map
}
Enter fullscreen mode Exit fullscreen mode

Nobody wants to write this, because it's two long, not fun, and error prone.

This is exactly when pattern matching comes in. Instead of checking every properties, you match you object with a "pattern" (a shape) and see if it match.

Pattern matching is not native in Typescript, but the closest we can do is probably "ts-pattern" (that is excellent)

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

...


export function UserProfile({ profile }: { profile: UserProfile }) {
  return (
    <h1> Hello { profile.firstname } </h1>
    {match(profile)
      .with({ address: { location: [P.number, P.number] } }, (location) => {
        return <Map lat={profile.address.location[0]} lng={profile.address.location[1]} />
      })
      .otherwise(() => {
        return null;
      })
    }
  )
}
Enter fullscreen mode Exit fullscreen mode

Using a value after matching it is so common there is a syntax to destructure the result of our matching:

match(x)
  .with({ address: { location: [P.number.select('lat'), P.number.select('lng')] } }, ({ lat, lng} ) => {
    return <Map lat={lat} lng={lng} />
  })
  .otherwise(() => {
    return null;
  })

Enter fullscreen mode Exit fullscreen mode

This is a very clean approach to replace a group of "if".

I found that this way of thinking rest my brain because I don't need to think to every branching, I describe my need, if feels more natural.

Once understood, we see a lot of opportunities to use pattern matching. For example, If you want to show a special message when the user is named "Bob" and is age is greater than 18.

You can write something like:

{
  match(profile)
    .with({ firstname: "Bob", age: P.number.gte(18).select() }, (age) => {
      return `Hello Bob, you are now ${age}, and you can drink a beer !`;
    })
    .otherwise(() => null);
}
Enter fullscreen mode Exit fullscreen mode

There is plenty different ways to match everything, you should check: https://github.com/gvergnaud/ts-pattern

Top comments (0)