patturn
The missing match
expression for JavaScript. Use functional pattern matching constructs familiar from languages like Rust to sidestep the limitations of switch
statements, reduce messy ternary expressions and if/else chains, and assign variables based on arbitrarily complex conditions.
Installation
yarn add patturn
or
npm install --save patturn
Match expressions
The match()
function behaves like a superpowered switch
statement that returns a value from the matched branch. It accepts a value to match against, an array of matchers, and an optional default return value.
import { match } from "patturn";
const ANSWER = 42;
const result = match(
ANSWER,
[
[0, "zilch"],
[3, "the magic number"],
[42, "the meaning of life"],
[420, "nothing to see here, officer"],
],
"no match"
);
// result: "the meaning of life"
The return types may be heterogeneous, and when using TypeScript can be inferred, or constrained as needed.
Guards and Returns
Each matcher consists of a guard and a return. Guards check if a value matches a condition, and returns specify the value to return from the match. Each can be a value, expression, array of values, function called with the value, or any combination thereof:
import { match } from "patturn";
const name = "benedict";
match(name, [
["thomas", "t-bone"],
[(n) => n.includes("ben"), `${name} cumberbatch?`],
[(n) => n.length > 8, (_n) => "too long, don't care"],
]); // returns "benedict cumberbatch?"
Note: guards use strict equality, or the boolean return value if a function.
Guard Lists
To match multiple values in a single match branch, simply pass in an array of values as the guard. This is the equivalent of the fallthrough behavior in switch
, and any matching value will immediately break with the associated return:
import { match } from "patturn";
const flavor = "strawberry";
const preference = match(
flavor,
[
[["chocolate", "vanilla"], "obviously good"],
[["mint chip", "strawberry"], "kinda okay"], // matches second guard case
["pistachio", "lowkey favorite"],
["rocky road", "too much going on"],
],
"no opinion"
);
// preference: "kinda okay"
Order matters
Ordering of matchers is important -- the first guard to pass is the one used. In the example below, both the third and fourth guards would pass, but the fourth is never run:
import { match } from "patturn";
type User = { name: string; id: number };
const me: User = { name: "rekt", id: 32 };
match<User, boolean | null>(me, [
[(u) => u.id === 1, true],
[(u) => u.name === "he-who-must-not-be-named", null],
[(u) => u.id < 1000, true],
[(u) => u.name === "rekt", false],
]); // returns `true`
Match Signature
function match<In, Out = In>(input: In, matchers: Array<MatchBranch<In, Out>>, defaultValue?: Out): Out | undefined;
type MatchBranch<In, Out> = [Guard<In>, Return<In, Out>];
type Guard<In> = In | In[] | ((input: In) => boolean);
type Return<In, Out> = Out | ((input: In) => Out);
When statements
The when()
function behaves much like match()
, but doesn't return a value. It has the added option of running lazily, stopping after the first match, or greedily and running through every match. It's also a lot like a switch
, useful for running side-effects based on complex conditions.
import { when } from "patturn";
const album = { artist: "Radiohead", title: "OK Computer", year: 1997 };
when(
album,
[
[
(a) => a.year >= 1990 && a.year <= 2000,
(_) => console.log("playing 90's music..."),
],
[(a) => a.artist === "Sisqo", () => process.exit(1)],
[(a) => a.artist === "Radiohead", () => setVolume(100)],
],
false
);
// - logs "playing 90's music..."
// - blasts volume
When Signature
function when<In>(input: In, matchers: Array<WhenBranch<In>>, lazy?: boolean): void;
type WhenBranch<In> = [Guard<In>, (input: In) => void];
type Guard<In> = In | In[] | ((input: In) => boolean);
License
MIT © Tobias Fried