Why do I use ts-pattern?

When I started learning Erlang, I found a very beautiful mechanism called pattern matching which by the way is known in many other languages too. The pattern-matching is such an important mechanism in some languages that in many cases it is a central feature where other things are extended from it. For example, the if-else is only an extension to the pattern-matching mechanism on elixir.

Pattern Matching in Erlang

So, when we are using a case expression in Erlang the syntax is like below and I was very glad to find that:

admit(Person) ->
    case Person of
        {male, Age} when Age >= 21 ->
            yes_with_cover;
        {female, Age} when Age >= 21 ->
            yes_no_cover;
        {male, _} ->
            no_boy_admission;
        {female, _} ->
            no_girl_admission;
        _ ->
            unknown
    end.

You can notice singular things in this example, like the _ and partial checking over values. I considered it very interesting and very useful, then my first reaction to it was to search for this mechanism in TypeScript or JavaScript context.

In my results, I found a proposal-pattern-matching about it on JavaScript, but it is in stage 1 yet, unfortunately. And I found a lib called ts-pattern which I really loved and started my tests with it.

Why pattern matching?

You might think “why would I use that? It is very similar to switch case statement (?)” and I agree with it most of the time, but we have some problems where pattern matching can solve it better. When we are using the switch we are limited to using a simple equality comparison. For example, when you are doing this:

const value: string = "any value";

if (value === "any value") {
    console.log("any");
} else if (value === "another value") {
    console.log("another");
} else {
    console.log("I don't know");
}

Well, we can simply transform to:

const value: string = "any value";

switch (value) {
    case "any value": 
        console.log("any");
        break;
    case "another value":
        console.log("another");
        break;
    default:
        console.log("I don't know");
        break;
}

But then suddenly, we have a second example, like below, where we have more conditions and branching decisions. Decisions depend on other decisions, it is a non-readable code.

const person = {
    gender: "male",
    age: 18
};

if (person.gender === "male" && person.age >= 21) {
    console.log("yes with cover");
} else if (person.gender === "female" && person.age >= 21) {
    console.log("yes no cover");
} else if (person.gender === "male" && person.age < 21) {
    console.log("no boy admission");
} else if (person.gender === "female" && person.age < 21) {
    console.log("no girl admission");
} else {
    console.log("unknown");
}

I know that it is a silly example, and we can refactor it better, but think of it like a switch case and well, you cannot because we have limits here. But we can refactor it using pattern matching in a very simple and sophisticated way:

return match(person)
    .with({ 
        gender: 'male', 
        age: P.when((x) => x >= 21) 
    }, () => console.log("yes with cover"))
    .with({
        gender: 'female', 
        age: P.when((x) => x >= 21) 
    }, () => console.log("yes no cover"))
    .with({
        gender: 'male', 
        age: P._
    }, () => console.log("no boy admission"))
    .with({ 
        gender: 'female', 
        age: P._
    }, () => console.log("no girl admission"))
    .exhaustive();

You can notice that pattern matching turns the code into a more readable and easier to maintain one. When you need to change the logic in a long codebase you know where to check and change.

You can also notice the exhaustive term and it was a very important detail to my decision to use pattern matching in my code.

Exhaustiveness

Exhaustiveness is a very good concept that makes your code safer to add new code. Basically, your compiler (in our case given from ts-pattern) knows all cases in your code and can warn you if you forget some there. It is very good for your codebase because it prevents you from forgetting to add something or deleting important code.

Then when you are coding and forget some cases, an error will be launched if you are running by exhaustive(), but you can ignore it.

JSX

The use with JSX is very good too and maybe your code can be better with it. If you are using many ternaries in your code, you can refactor it like below, turning your code into more readable and maintenance-friendly.

<div>
    {match(state)
        .with({ status: "idle" }, () => (
            <Idle />
        ))
        .with({ status: "loading" }, () => (
            <Loading />
        ))
        .with({ status: "error", message: when(m => !!m) }, ({ message }) => (
            <Error>{message}</Error>
        ))
        .with({ status: "error", message: _ }, () => (
            <Error>Unknown error</Error>
        ))
    .exhaustive()}
</div>

Considerations

Here we saw some simple examples about the usage for pattern matching, but the usage is bigger than it, and you can explore and maybe or certainly improve your code with it.