match-iz

A tiny pattern-matching library in the style of the TC39 proposal.
GitHub
156
Created 4 years ago, last commit 14 days ago
3 contributors
478 commits
Stars added on GitHub, month by month
N/A
N/A
2
3
4
5
6
7
8
9
10
11
12
1
2024
2025
Stars added on GitHub, per day, on average
Yesterday
=
Last week
0.0
/day
Last month
+0.1
/day
npmPackage on NPM
Monthly downloads on NPM
2
3
4
5
6
7
8
9
10
11
12
1
2024
2025
No dependencies
README

match-iz ๐Ÿ”ฅ

MIT license Downloads per week npm bundle size Version

A tiny functional, declarative pattern-matching library.

Introduction

Pattern-matching is a declarative version of if and switch, where you describe the expected shape of your data using "patterns".

import { match, when, otherwise } from 'match-iz'

let result = match(data)(
  when(pattern, result || handler),
  when(pattern, result || handler),
  otherwise(result || handler)
)

Patterns are a combination of both functions and data, and because of this certain assumptions can be made by match-iz to help reduce the amount of boilerplate normally required to check that your data looks a certain way:

// Imperative:
if (typeof res?.statusCode === 'number') {
  if (res.statusCode >= 200 && res.statusCode < 300) {
    return res.body
  }
}

// Declarative:
return match(res)(
  when({ statusCode: inRange(200, 299) }, () => res.body),
  otherwise(() => {})
)
  1. match-iz will check that statusCode is a key of res by implication of the when() being passed an object-literal { ... }.

  2. The inRange() pattern-helper guards against non-numbers before trying to determine if its input is within a certain range.

Many pattern-helpers are provided to permit you to express terse, declarative, and reusable (just pop them into variables/constants) patterns.

Here are some of the date ones:

const isLastSundayOfMarch = allOf(nthSun(-1), isMar)
const isTheWeekend = anyOf(allOf(isFri, isEvening), isSat, isSun)

match(new Date())(
  when(isLastSundayOfMarch, () => 'Last Sunday of March: Clocks go forward'),
  when(isTheWeekend, () => 'Ladies and Gentlemen; The Weekend'),
  otherwise(dateObj => {
    return `The clock is ticking: ${dateObj.toString()}`
  })
)

rest was introduced in v5:

// For objects, use ...rest()
match({ one: 1, two: 2, three: 3 })(
  when({ one: 1, ...rest(isNumber) }, (_, rest) => {
    console.log(rest);
    // { two: 2, three: 3 }
  }),
)

// For arrays, use rest()
match([1, 2, 3])(
  when([1, rest(isNumber)], (_, rest) => {
    console.log(rest);
    // [2, 3]
  }),
)

You can browse a few more examples below, and full documentation is over on the Github Wiki.

Before / After Examples:

getResponse | Testing status-codes:

See imperative equivalent
function getResponse(res) {
  if (res && typeof res.statusCode === 'number') {
    if (res.statusCode >= 200 && res.statusCode < 300) {
      return res.body
    } else if (res.statusCode === 404) {
      return 'Not found'
    }
  }
  throw new Error('Invalid response')
}
function getResponse(res) {
  return match(res)(
    when({ statusCode: inRange(200, 299) }, () => res.body),
    when({ statusCode: 404 }, () => 'Not found'),
    otherwise(res => {
      throw new Error(`Invalid response: ${res}`)
    })
  )
}

performSearch | "Overloaded" function call:

See imperative equivalent
function performSearch(...args) {
  const [firstArg, secondArg] = args
  if (args.length === 1) {
    if (isString(firstArg)) {
      return find({ pattern: firstArg })
    }
    if (isPojo(firstArg)) {
      return find(firstArg)
    }
  }
  if (args.length === 2 && isString(firstArg) && isPojo(secondArg)) {
    return find({ pattern: firstArg, ...secondArg })
  }
  throw new Error('Invalid arguments')
}
function performSearch(...args) {
  return match(args)(
    when(eq([isString]), ([pattern]) => find({ pattern })),
    when(eq([isPojo]), ([options]) => find(options)),
    when(eq([isString, isPojo]), ([pattern, options]) =>
      find({ pattern, ...options })
    ),
    otherwise(() => {
      throw new Error('Invalid arguments')
    })
  )
}

AccountPage | React Component:

See imperative equivalent
function AccountPage(props) {
  const { loading, error, data } = props || {}
  const logout = !loading && !error && !data
  return (
    <>
      {loading && <Loading />}
      {error && <Error {...props} />}
      {data && <Page {...props} />}
      {logout && <Logout />}
    </>
  )
}
function AccountPage(props) {
  return match(props)(
    when({ loading: defined }, <Loading />),
    when({ error: defined }, <Error {...props} />),
    when({ data: defined }, <Page {...props} />),
    otherwise(<Logout />)
  )
}

calculateExpr | Regular Expressions:

See imperative equivalent
function calculateExpr(expr) {
  const rxAdd = /(?<left>\d+) \+ (?<right>\d+)/
  const rxSub = /(?<left>\d+) \- (?<right>\d+)/
  if (typeof expr === 'string') {
    const addMatch = expr.match(rxAdd)
    if (addMatch) {
      const { left, right } = addMatch.groups
      return add(left, right)
    }
    const subMatch = expr.match(rxAdd)
    if (subMatch) {
      const { left, right } = subMatch.groups
      return subtract(left, right)
    }
  }
  throw new Error("I couldn't parse that!")
}
function calculateExpr(expr) {
  return match(expr)(
    when(/(?<left>\d+) \+ (?<right>\d+)/, groups =>
      add(groups.left, groups.right)
    ),
    when(/(?<left>\d+) \- (?<right>\d+)/, groups =>
      subtract(groups.left, groups.right)
    ),
    otherwise("I couldn't parse that!")
  )
}

Benchmarks

There is a very small benchmarking suite that you can run yourself with:

pnpm run bench

Here's a run to give you an example without needing to go anywhere else:

calculateExpr_vanilla         165.63 ns/iter 163.72 ns  โ–ˆ
                     (160.47 ns โ€ฆ 194.63 ns) 187.64 ns  โ–ˆ
                     (603.13 kb โ€ฆ 608.66 kb) 512.93 kb โ–„โ–ˆโ–‡โ–‚โ–‚โ–‚โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–‚โ–ƒโ–‚โ–‚โ–โ–
calculateExpr_matchiz_match   436.82 ns/iter 441.31 ns        โ–ˆ
                     (418.42 ns โ€ฆ 689.60 ns) 479.57 ns  โ–…     โ–ˆโ–ƒ
                     (  2.65 mb โ€ฆ   2.86 mb) 879.46 kb โ–†โ–ˆโ–ƒโ–‚โ–โ–‚โ–‡โ–ˆโ–ˆโ–„โ–‚โ–โ–โ–โ–โ–โ–โ–โ–โ–โ–
calculateExpr_matchiz_against 380.36 ns/iter 389.14 ns  โ–ˆโ–…
                     (368.65 ns โ€ฆ 409.00 ns) 398.41 ns  โ–ˆโ–ˆโ–…         โ–ˆโ–ˆ
                     (  1.71 mb โ€ฆ   1.73 mb) 987.37 kb โ–„โ–ˆโ–ˆโ–ˆโ–‡โ–„โ–ƒโ–โ–โ–‚โ–‚โ–โ–…โ–ˆโ–ˆโ–ˆโ–ˆโ–ƒโ–ƒโ–ƒโ–
calculateExpr_tspattern       803.26 ns/iter 878.30 ns  โ–„           โ–ˆ
                       (632.93 ns โ€ฆ 1.24 ยตs) 995.19 ns  โ–ˆ           โ–ˆโ–ˆ
                     (  2.32 mb โ€ฆ   2.34 mb) 810.93 kb โ–‡โ–ˆโ–โ–โ–โ–โ–โ–โ–โ–โ–‚โ–โ–†โ–ˆโ–ˆโ–ƒโ–‚โ–โ–โ–‚โ–‚

                              โ”Œ                                            โ”
        calculateExpr_vanilla โ”ค 165.63 ns
  calculateExpr_matchiz_match โ”คโ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ–  436.82 ns
calculateExpr_matchiz_against โ”คโ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ–  380.36 ns
      calculateExpr_tspattern โ”คโ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ– โ–  803.26 ns
                              โ””                                            โ”˜

This compares match-iz with ts-pattern.

Of course, when considering a library performance isn't the only thing that might concern you. ts-pattern can calculate static-types for the patterns described, while match-iz was written with JavaScripts dynamism in mind, and its TypeScript support is very basic and incomplete.

Install / Use:

$ pnpm i match-iz
// ESM
import { match, ...etc } from 'match-iz'
import { isSat, ...etc } from 'match-iz/dates'
import { isSat, ...etc } from 'match-iz/dates/utc'

// CJS
const { match, ...etc } = require('match-iz')

Browser/UMD:

<script src="https://unpkg.com/match-iz/dist/match-iz.browser.js"></script>
<script>
  const { match, ...etc } = matchiz
  const { isSat, ...etc } = matchiz
  const { isSat, ...etc } = matchiz.utc
</script>

Documentation

Check out the Github Wiki for complete documentation of the library.

Credits

match-iz was written by Conan Theobald.

I hope you found it useful! If so, I like coffee โ˜•๏ธ :)

License

MIT licensed: See LICENSE