patturn
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

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.

NPM npm bundle size

GitHub stars GitHub forks GitHub watchers Follow on GitHub

Installation

npm install patturn
#^ Or whatever package manager you use

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, and any number of match arms. When executed it returns the value in the matched arm.

import { match } from "patturn";

const answer = 42;
const result = match(answer)
  .with(0, "zilch")
  .with(3, "the magic number")
  .with(42, "the meaning of life")
  .with(420, "nothing to see here, officer")
  .execute(); // "the meaning of life"

The return types may be heterogeneous, and when using TypeScript, they also serve to constrain the test value's type on success. can be inferred, or constrained as needed.

Guards and Returns

Each match arm (calls to with) 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. Guards can be a value, Pattern, array of values, a function returning a boolean, or any combination thereof:

const name = "benedict";

match(name)
  .with("thomas", "t-bone")
  .with((n) => n.includes("ben"), `${name} cumberbatch?`)
  .with(
    (n) => n.length > 8,
    (n) => n.length
  )
  .execute(); // "benedict cumberbatch?"

Note: guards use strict equality checks when the value is primitive, or the boolean return value if a function.

Matching a non-primitive value like an Object, Array, or class instance comes in a few flavors. To loosely match objects with certain properties, a simple guard object will suffice. Loose matching only requires that properties on the guard match properties on the test value, not vice-versa. For strict object matching where all own enumerable properties are matched, use P.object and associated patterns. To match an array, use P.array and associated patterns (array literals behave like "any of these" — see Guard Lists):

import { match, P } from "patturn";

const testval = { foo: true, bar: [1, 2, 3], baz: "hello world" };

match(testval)
  .with({ foo: false }, "no")
  .with({ foo: true, bar: P.array.of(P.number) }, "yep") // <-- matches!
  .with(P.object.strict({ baz: "hello world" }), "nope")
  .execute(); // "yep"

Guard Lists

To match multiple possible 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:

const flavor = "strawberry";

const preference = match(flavor)
  .with(["chocolate", "vanilla"], "obviously good")
  .with(["mint chip", "strawberry"], "kinda okay") // <-- matches!
  .with("pistachio", "lowkey favorite")
  .with("rocky road", "too much going on")
  .execute(); // "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:

type User = { name: string; id: number };
const me: User = { name: "rekt", id: 32 };

match(me)
  .with((u) => u.id === 1, true)
  .with((u) => u.name === "he-who-must-not-be-named", null)
  .with((u) => u.id < 1000, "yes") // <-- matches first!
  .with((u) => u.name === "rekt", false)
  .execute(); // "yes"

Return expressions

You may provide a literal return value in a match arm, or supply a function to be called with the match value to produce a return value. This is useful to keep matching and transforming logic collocated, while saving work due to the lazy nature of match arms:

const testval = ["age", "quod", "agis"];

match(testval)
  .with(P.array.len(2), (arr) => arr[0])
  .with(300, (n) => n ** 2)
  .with(P.array.of(P.string), (arr) => arr.map((str) => str.toUpperCase()))
  .execute(); // ["AGE", "QUOD", "AGIS"]

Patterns

Patterns are simple data validation functions that return true if the given input matches, otherwise false. Some Patterns have additional, chainable predicates that help refine and constrain validation. In TypeScript, they also serve to constrain the test value's type on success. Patterns live on the P object

P._

The wildcard pattern, this function matches any input and always returns true no matter the input. It is also available under the alias P.any.

P.nullish

Matches null or undefined inputs.

P.boolean

Matches boolean primitives and Boolean objects.

Examples

P.boolean(true); // true
P.boolean(false); // true
P.boolean(new Boolean()); // true
P.boolean("true"); // false

P.falsy

Matches any falsy value, namely false, undefined, null, 0, -0, NaN, and "" (the empty string).

P.truthy

Matches any truthy value, namely all values other than false, undefined, null, 0, -0, NaN, and "" (the empty string).

P.string

Matches string primitives and String objects.

Modifiers

  • P.string.includes(substr: string): string includes a given substr
  • P.string.startsWith(prefix: string): string starts with a given prefix
  • P.string.endsWith(suffix: string): string starts with a given suffix
  • P.string.uppercase: string contains only uppercase alphabetic characters
  • P.string.lowercase: string contains only lowercase alphabetic characters
  • P.string.alphabetic: string contains only alphabetic characters
  • P.string.alphanumeric: string contains only alphanumeric characters
  • P.string.numeric: string contains only numeric characters
  • P.string.url: string is a string representation of a valid URL (including protocol)
    • P.string.url.loose: string is a string URL representation which may or may not have a protocol
  • P.string.enum(values: string[]): string is one of a finite set of values
  • P.string.len(len: number | [min: number | null, max?: number | null]): string is a string of length len, or between min and max characters in length (inclusive) if len is a tuple.

Examples

P.string("mario"); // true
P.string(new String("mario")); // true
P.string.includes("a")("mario"); // true
P.string.startsWith("a")("mario"); // false
P.string.endsWith("o")("mario"); // true
P.string.uppercase("mario"); // false
P.string.lowercase("mario"); // true
P.string.alphabetic("mario"); // true
P.string.alphanumeric("mario"); // true
P.string.numeric("mario"); // false
P.string.url("mario.com"); // false
P.string.url.loose("mario.com"); // true
P.string.enum(["mario", "luigi", "toad"])("mario"); // true
P.string.len(10)("mario"); // false
P.string.len([3, 10])("mario"); // true

P.regex

P.number

P.bigint

P.symbol

P.object

P.array

P.tuple

P.function

P.contains

P.union

P.intersection

P.instanceOf

Modifiers

Async Match Expressions

For cases when asynchronous checks or return mappings are needed, use the matchAsync function as you would match. As with the sync form, cases are evaluated lazily in sequence. It handles the same synchronous guards and returns, as well as Promises and async functions:

import { matchAsync } from "patturn";

const signupState = await matchAsync({
  username: "something_rude",
  email: "person@domain.com",
})
  .with({ username: "", email: "" }, SignupState.Empty) // ✔ value
  .with(isEmailInvalid, SignupState.EmailInvalid) // ✔ sync fn
  .with(isUsernameInvalid, SignupState.UsernameInvalid) // ✔ async fn
  .with(Promise.resolve(false), SignupState.Never) // ✔ promise
  .otherwise(SignupState.Ok);

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 (exhaustively) and running through every match. It's also a lot like a switch, useful for running side-effects based on complex conditions.

const album = { artist: "Radiohead", title: "OK Computer", year: 1997 };

when(album)
  .is({ year: P.number.between(1990, 2000) }, (_) =>
    console.log("playing 90's music...")
  )
  .is(
    (a) => a.artist === "Sisqo",
    () => process.exit(1)
  )
  .is(
    (a) => a.artist === "Radiohead",
    () => setVolume(100)
  )
  .lazy();

// - logs "playing 90's music..."

Early Returns

Sometimes you want to break out of pattern matching early, without running any side-effects or responding in any particular way. In this case, just omit the handler from the matcher and use lazy matching. This is analagous to a switch arm with only a break statement:

when(23)
  .is(42, submitAnswer)
  .is(
    (n) => n % 9 === 0,
    (n) => bottleBeers(n + 1)
  )
  .is(isPrime) // early return with no operation to perform
  .is(600) // early return with no operation to perform
  .is(-1, doSomething)
  .otherwise(fallback); // must be exhaustive

Async When statements

As expected, the async form whenAsync can match and run arbitrary patterns, Promises, and async functions:

import { whenAsync } from "patturn";

await whenAsync(33)
  .is(101, () => console.log("needs help"))
  .is(isPrimeAsync, handlePrimeCase)
  .is(
    async (n) => longComputationReturningBool,
    (n) => handlePass(n, "xyz")
  )
  .lazy();

License

MIT © Tobias Fried

Package Sidebar

Install

npm i patturn

Weekly Downloads

0

Version

1.1.0

License

MIT

Unpacked Size

54.9 kB

Total Files

13

Last publish

Collaborators

  • rektdeckard