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",
}
}
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;
};
};
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]} />
);
}
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]
}
}
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 keyaddress
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
}
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;
})
}
)
}
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;
})
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);
}
There is plenty different ways to match everything, you should check: https://github.com/gvergnaud/ts-pattern