patcom
April 20, 2022 · View on GitHub
patcom is a pattern-matching JavaScript library. Build pattern matchers from simpler, smaller matchers.
Pattern-matching uses declarative programming. The code matches the shape of the data.
npm install --save patcom
Simple example
Let's say we have objects that represent a Student or a Teacher.
type Student = {
role: 'student'
}
type Teacher = {
role: 'teacher',
surname: string
}
Using patcom, we can match a person by their role to form a greeting.
import {match, when, otherwise, defined} from 'patcom'
function greet(person) {
return match (person) (
when (
{ role: 'student' },
() => 'Hello fellow student.'
),
when (
{ role: 'teacher', surname: defined },
({ surname }) => `Good morning ${surname} sensei.`
),
otherwise (
() => 'STRANGER DANGER'
)
)
}
greet({ role: 'student' }) ≡ 'Hello fellow student.'
greet({ role: 'teacher', surname: 'Wong' }) ≡ 'Good morning Wong sensei.'
greet({ role: 'creeper' }) ≡ 'STRANGER DANGER'
What is match doing?
match finds the first when clause that matches, then the Matched object is transformed into the greeting. If none of the when clauses match, the otherwise clause always matches.
More expressive than switch
Pattern match over whole objects and not just single fields.
Imperative switch & if 😔
Oh noes, a Pyramid of doom
switch (person.role) {
case 'student':
if (person.grade > 90) {
return 'Gold star'
} else if (person.grade > 60) {
return 'Keep trying'
} else {
return 'See me after class'
}
default:
throw new Exception(`expected student, but got ${person}`)
}
Declarative match 🙂
Flatten Pyramid to linear cases.
return match (person) (
when (
{ role: 'student', grade: greaterThan(90) },
() => 'Gold star'
),
when (
{ role: 'student', grade: greaterThan(60) },
() => 'Keep trying'
),
when (
{ role: 'student', grade: defined },
() => 'See me after class'
),
otherwise (
(person) => throw new Exception(`expected student, but got ${person}`)
)
)
What is greaterThan?
greaterThan is a Matcher provided by patcom. greaterThan(90) means "match a number greater than 90".
Match Array, String, RegExp and more
Arrays
match (list) (
when (
[],
() => 'empty list'
),
when (
[defined],
([head]) => `single item ${head}`
),
when (
[defined, rest],
([head, tail]) => `multiple items`
)
)
What is rest?
rest is an IteratorMatcher used within array and object patterns. Array and objects are complete matches, and the rest pattern consumes all remaining values.
String & RegExp
match (command) (
when (
'sit',
() => sit()
),
// matchedRegExp is the RegExp match result
when (
/^move (\d) spaces$/,
(value, { matchedRegExp: [, distance] }) => move(distance)
),
// ...which means matchedRegExp has the named groups
when (
/^eat (?<food>\w+)$/,
(value, { matchedRegExp: { groups: { food } } }) => eat(food)
)
)
Number, BigInt & Boolean
match (value) (
when (
69,
() => 'nice'
),
when (
69n,
() => 'big nice'
),
when (
true,
() => 'not nice'
)
)
Match complex data structures
match (complex) (
when (
{ schedule: [{ class: 'history', rest }, rest] },
() => 'history first thing on schedule? buy coffee'
),
when (
{ schedule: [{ professor: oneOf('Ko', 'Smith'), rest }, rest] },
({ schedule: [{ professor }] }) => `Professor ${professor} teaching? bring voice recorder`
)
)
Matchers are extractable
From the previous example, complex patterns can be broken down into simpler reusable matchers.
const fastSpeakers = oneOf('Ko', 'Smith')
match (complex) (
when (
{ schedule: [{ class: 'history', rest }, rest] },
() => 'history first thing on schedule? buy coffee'
),
when (
{ schedule: [{ professor: fastSpeakers, rest }, rest] },
({ schedule: [{ professor }] }) => `Professor ${professor} teaching? bring voice recorder`
)
)
Custom matchers
Define custom matchers with any logic. ValueMatcher is a helper function to define custom matchers. It wraps a function that takes in a value and returns a Result. Either the value becomes Matched or is Unmatched.
const matchDuck = ValueMatcher((value) => {
if (value.type === 'duck') {
return {
matched: true,
value
}
}
return {
matched: false
}
})
...
function speak(animal) {
return match (animal) (
when (
matchDuck,
() => 'quack'
),
when (
matchDragon,
() => 'rawr'
)
)
)
All the examples thus far have been using match, but match itself isn't a matcher. In order to use speak in another pattern, we use oneOf instead.
const speakMatcher = oneOf (
when (
matchDuck,
() => 'quack'
),
when (
matchDragon,
() => 'rawr'
)
)
Now upon unrecognized animals, whereas speak previously returned undefined, speakMatcher returns { matched: false }. This allows us to combine speakMatcher with other patterns.
match (animal) (
when (
speakMatcher,
(sound) => `the ${animal.type} goes ${sound}`
),
otherwise(
() => `the ${animal.type} remains silent`
)
)
Everything except for match is actually a Matcher, including when and otherwise. Primitive value and data types are automatically converted to a corresponding matcher.
when ({ role: 'student' }, ...) ≡
when (matchObject({ role: 'student' }), ...)
when (['alice'], ...) ≡
when (matchArray(['alice']), ...)
when ('sit', ...) ≡
when (matchString('sit'), ...)
when (/^move (\d) spaces$/, ...) ≡
when (matchRegExp(/^move (\d) spaces$/), ...)
when (69, ...) ≡
when (matchNumber(69), ...)
when (69n, ...) ≡
when (matchBigInt(69n), ...)
when (true, ...) ≡
when (matchBoolean(true), ...)
Even the complex patterns are composed of simpler matchers.
Primitives
when (
{
schedule: [
{ class: 'history', rest },
rest
]
},
...
)
Equivalent explict matchers
when (
matchObject({
schedule: matchArray([
matchObject({ class: matchString('history'), rest }),
rest
])
}),
...
)
Core concept
At the heart of patcom, everything is built around a single concept, the Matcher. The Matcher takes any value and returns a Result, which is either Matched or Unmatched. Internally, the Matcher consumes a TimeJumpIterator to allow for lookahead.
Custom matchers are easily implemented using the ValueMatcher helper function. It removes the need to handle the internals of TimeJumpIterator.
type Matcher<T> = (value: TimeJumpIterator<any> | any) => Result<T>
function ValueMatcher<T>(fn: (value: any) => Result<T>): Matcher<T>
type Result<T> = Matched<T> | Unmatched
type Matched<T> = {
matched: true,
value: T
}
type Unmatched = {
matched: false
}
For more advanced use cases, the IteratorMatcher helper function is used to create Matchers that directly handle the internals of TimeJumpIterator but do not need to be concerned with a plain value being passed in.
The TimeJumpIterator works like a normal Iterator, except it can jump back to a previous state. This is useful for Matchers that require lookahead. For example, the maybe matcher would remember the starting position with const start = iterator.now, look ahead to see if there is a match, and if it fails, jumps the iterator back using iterator.jump(start). This prevents the iterator from being consumed. If the iterator is consumed during the lookahead and left untouched on unmatched, subsequent matchers will fail to match as they would never see the values that were consumed by the lookahead.
function IteratorMatcher<T>(fn: (value: TimeJumpIterator<any>) => Result<T>): Matcher<T>
type TimeJumpIterator<T> = Iterator<T> & {
readonly now: number,
jump(time: number): void
}
Use the asInternalIterator to pass an existing iterator into a Matcher.
const matcher = group('a', 'b', 'c')
matcher(asInternalIterator('abc')) ≡ {
matched: true,
value: ['a', 'b', 'c'],
result: [
{ matched: true, value: 'a' },
{ matched: true, value: 'b' },
{ matched: true, value: 'c' }
]
}
Built-in Matchers
Directly useable Matchers.
-
anyconst any: Matcher<any>Matches for any value, including
undefined.Example
const matcher = any matcher(undefined) ≡ { matched: true, value: undefined } matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' } } -
definedconst defined: Matcher<any>Matches for any defined value, or in other words not
undefined.Example
const matcher = defined matcher({ key: 'value' }) ≡ { matched: true, value: {key: 'value' } } matcher(undefined) ≡ { matched: false } -
emptyconst empty: Matcher<[] | {} | ''>Matches either
[],{}, or''(empty string).Example
const matcher = empty matcher([]) ≡ { matched: true, value: [] } matcher({}) ≡ { matched: true, value: {} } matcher('') ≡ { matched: true, value: '' } matcher([42]) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false } matcher('alice') ≡ { matched: false }
Matcher builders
Builders to create a Matcher.
-
betweenfunction between(lower: number, upper: number): Matcher<number>Matches if value is a
Number, wherelower <= value < upperExample
const matcher = between(10, 20) matcher(9) ≡ { matched: false } matcher(10) ≡ { matched: true, value: 10 } matcher(19) ≡ { matched: true, value: 19 } matcher(20) ≡ { matched: false } -
equalsfunction equals<T>(expected: T): Matcher<T>Matches
expectedif strictly equals===to value.Example
const matcher = equals('alice') matcher('alice') ≡ { matched: true, value: 'alice' } matcher(42) ≡ { matched: false }const matcher = equals(42) matcher('alice') ≡ { matched: false } matcher(42) ≡ { matched: true, value: 42 }const matcher = equals(undefined) matcher(undefined) ≡ { matched: true, value: undefined } matcher(42) ≡ { matched: false } -
greaterThanfunction greaterThan(expected: number): Matcher<number>Matches if value is a
Number, whereexpected < valueExample
const matcher = greaterThan(10) matcher(9) ≡ { matched: false } matcher(10) ≡ { matched: false } matcher(11) ≡ { matched: true, value: 11 } -
greaterThanEqualsfunction greaterThanEquals(expected: number): Matcher<number>Matches if value is a
Number, whereexpected <= valueExample
const matcher = greaterThanEquals(10) matcher(9) ≡ { matched: false } matcher(10) ≡ { matched: true, value: 10 } matcher(11) ≡ { matched: true, value: 11 } -
lessThanfunction lessThan(expected: number): Matcher<number>Matches if value is a
Number, whereexpected > valueExample
const matcher = lessThan(10) matcher(9) ≡ { matched: true, value: 9 } matcher(10) ≡ { matched: false } matcher(11) ≡ { matched: false } -
lessThanEqualsfunction lessThanEquals(expected: number): Matcher<number>Matches if value is a
Number, whereexpected >= valueExample
const matcher = lessThanEquals(10) matcher(9) ≡ { matched: true, value: 9 } matcher(10) ≡ { matched: true, value: 10 } matcher(11) ≡ { matched: false } -
matchPredicatefunction matchPredicate<T>(predicate: (value: any) => Boolean): Matcher<T>Matches value that satisfies the predicate, or in other words
predicate(value) === trueExample
const isEven = (x) => x % 2 === 0 const matcher = matchPredicate(isEven) matcher(2) ≡ { matched: true, value: 2 } matcher(1) ≡ { matched: false } -
matchBigIntfunction matchBigInt(expected?: bigint): Matcher<bigint>Matches if value is the
expectedBigInt. Matches any defined BigInt ifexpectedis not provided.Example
const matcher = matchBigInt(42n) matcher(42n) ≡ { matched: true, value: 42n } matcher(69n) ≡ { matched: false } matcher(42) ≡ { matched: false }const matcher = matchBigInt() matcher(42n) ≡ { matched: true, value: 42n } matcher(69n) ≡ { matched: true, value: 69n } matcher(42) ≡ { matched: false } -
matchNumberfunction matchNumber(expected?: number): Matcher<number>Matches if value is the
expectedNumber. Matches any definedNumberifexpectedis not provided.Example
const matcher = matchNumber(42) matcher(42) ≡ { matched: true, value: 42 } matcher(69) ≡ { matched: false } matcher(42n) ≡ { matched: false }const matcher = matchNumber() matcher(42) ≡ { matched: true, value: 42 } matcher(69) ≡ { matched: true, value: 69 } matcher(42n) ≡ { matched: false } -
matchPropfunction matchProp(expected: string): Matcher<string>Matches if value has
expectedas a property key, or in other wordsexpected in value.Example
const matcher = matchProp('x') matcher({ x: 42 }) ≡ { matched: true, value: { x: 42 } } matcher({ y: 42 }) ≡ { matched: false } matcher({}) ≡ { matched: false } -
matchStringfunction matchString(expected?: string): Matcher<string>Matches if value is the
expectedString. Matches any definedStringifexpectedis not provided.Example
const matcher = matchString('alice') matcher('alice') ≡ { matched: true, value: 'alice' } matcher('bob') ≡ { matched: false }const matcher = matchString() matcher('alice') ≡ { matched: true, value: 'alice' } matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false } -
matchRegExpfunction matchRegExp(expected: RegExp): Matcher<string>Matches if value matches the
expectedRegExp.Matchedwill include theRegExpmatch object as thematchedRegExpproperty.Example
const matcher = matchRegExp(/^dear (\w+)$/) matcher('dear alice') ≡ { matched: true, value: 'dear alice', matchedRegExp: ['dear alice', 'alice'] } matcher('hello alice') ≡ { matched: false }const matcher = matchRegExp(/^dear (?<name>\w+)$/) matcher('dear alice') ≡ { matched: true, value: 'dear alice', matchedRegExp: { groups: { name: 'alice' } } } matcher('hello alice') ≡ { matched: false }
Matcher composers
Creates a Matcher from other Matchers.
-
matchArrayfunction matchArray<T>(expected?: T[]): Matcher<T[]>Matches
expectedarray completely. Primitives inexpectedare wrapped with their correspondingMatcherbuilder.expectedarray can also includeIteratorMatchers which can consume multiple elements. Matches any defined array ifexpectedis not provided.Example
const matcher = matchArray([42, 'alice']) matcher([42, 'alice']) ≡ { matched: true, value: [42, 'alice'], result: [ { matched: true, value: 42 }, { matched: true, value: 'alice' } ] } matcher([42, 'alice', true, 69]) ≡ { matched: false } matcher(['alice', 42]) ≡ { matched: false } matcher([]) ≡ { matched: false } matcher([42]) ≡ { matched: false }const matcher = matchArray([42, 'alice', rest]) matcher([42, 'alice']) ≡ { matched: true, value: [42, 'alice', []], result: [ { matched: true, value: 42 }, { matched: true, value: 'alice' }, { matched: true, value: [] } ] } matcher([42, 'alice', true, 69]) ≡ { matched: true, value: [42, 'alice', [true, 69]], result: [ { matched: true, value: 42 }, { matched: true, value: 'alice' }, { matched: true, value: [true, 69] } ] } matcher(['alice', 42]) ≡ { matched: false } matcher([]) ≡ { matched: false } matcher([42]) ≡ { matched: false }const matcher = matchArray([maybe('alice'), 'bob']) matcher(['alice', 'bob']) ≡ { matched: true, value: ['alice', 'bob'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'bob' } ] } matcher(['bob']) ≡ { matched: true, value: [undefined, 'bob'], result: [ { matched: true, value: undefined }, { matched: true, value: 'bob' } ] } matcher(['eve', 'bob']) ≡ { matched: false } matcher(['eve']) ≡ { matched: false }const matcher = matchArray([some('alice'), 'bob']) matcher(['alice', 'alice', 'bob']) ≡ { matched: true, value: [['alice', 'alice'], 'bob'], result: [ { matched: true, value: ['alice', 'alice'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'alice' } ] }, { matched: true, value: 'bob' } ] } matcher(['eve', 'bob']) ≡ { matched: false } matcher(['bob']) ≡ { matched: false }const matcher = matchArray([group('alice', 'fred'), 'bob']) matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [['alice', 'fred'], 'bob'], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'eve', 'bob']) ≡ { matched: false } matcher(['eve', 'fred', 'bob']) ≡ { matched: false } matcher(['alice', 'bob']) ≡ { matched: false } matcher(['fred', 'bob']) ≡ { matched: false } matcher(['bob']) ≡ { matched: false }const matcher = matchArray() matcher([42, 'alice']) ≡ { matched: true, value: [42, 'alice'], result: [] } matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false } -
matchObjectfunction matchObject<T>(expected?: T): Matcher<T>Matches
expectedenumerable object properties completely or partially withrestmatcher. Primitives inexpectedare wrapped with their correspondingMatcherbuilder. The rest of properties can be found on thevaluewith the rest key. Matches any defined object ifexpectedis not provided.Example
const matcher = matchObject({ x: 42, y: 'alice' }) matcher({ x: 42, y: 'alice' }) ≡ { matched: true, value: { x: 42, y: 'alice' }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' } } } matcher({ y: 'alice', x: 42 }) ≡ { matched: true, value: { y: 'alice', x: 42 }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' } } } matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ { matched: false } matcher({}) ≡ { matched: false } matcher({ x: 42 }) ≡ { matched: false }const matcher = matchObject({ x: 42, y: 'alice', rest }) matcher({ x: 42, y: 'alice' }) ≡ { matched: true, value: { x: 42, y: 'alice', rest: {} }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, rest: { matched: true, value: {} } } } matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ { matched: true, value: { x: 42, y: 'alice', rest: { z: true, aa: 69 } }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, rest: { matched: true, value: { z: true, aa: 69 } } } } matcher({}) ≡ { matched: false } matcher({ x: 42 }) ≡ { matched: false }const matcher = matchObject({ x: 42, y: 'alice', customRestKey: rest }) matcher({ x: 42, y: 'alice', z: true }) ≡ { matched: true, value: { x: 42, y: 'alice', customRestKey: { z: true } }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, customRestKey: { matched: true, value: { z: true } } } }const matcher = matchObject() matcher({ x: 42, y: 'alice' }) ≡ { matched: true, value: { x: 42, y: 'alice' }, result: {} } matcher(undefined) ≡ { matched: false } matcher('alice') ≡ { matched: false } -
groupfunction group<T>(...expected: T[]): Matcher<T[]>An
IteratorMatcherthat consumes all a sequence of element matchingexpectedarray. Similar to regular expression group.Example
const matcher = matchArray([group('alice', 'fred'), 'bob']) matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [['alice', 'fred'], 'bob'], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'eve', 'bob']) ≡ { matched: false } matcher(['eve', 'fred', 'bob']) ≡ { matched: false } matcher(['alice', 'bob']) ≡ { matched: false } matcher(['fred', 'bob']) ≡ { matched: false } matcher(['bob']) ≡ { matched: false }const matcher = matchArray([group(maybe('alice'), 'fred'), 'bob']) matcher(['fred', 'bob']) ≡ { matched: true, value: [[undefined, 'fred'], 'bob'], result: [ { matched: true, value: [undefined, 'fred'], result: [ { matched: true, value: undefined }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [['alice', 'fred'], 'bob'], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] }const matcher = matchArray([group(some('alice'), 'fred'), 'bob']) matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [[['alice'], 'fred'], 'bob'], result: [ { matched: true, value: [['alice'], 'fred'], result: [ { matched: true, value: ['alice'], result: [ { matched: true, value: 'alice' } ] }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'alice', 'fred', 'bob']) ≡ { matched: true, value: [[['alice', 'alice'], 'fred'], 'bob'], result: [ { matched: true, value: [['alice', 'alice'], 'fred'], result: [ { matched: true, value: ['alice', 'alice'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'alice' } ] }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' } ] } matcher(['fred', 'bob']) ≡ { matched: false } -
maybefunction maybe<T>(expected: T): Matcher<T | undefined>An
IteratorMatcherthat consumes an element in the array if it matchesexpected, otherwise does nothing. The unmatched element can be consumed by the next matcher. Similar to the regular expression?operator.Example
const matcher = matchArray([maybe('alice'), 'bob']) matcher(['alice', 'bob']) ≡ { matched: true, value: ['alice', 'bob'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'bob' } ] } matcher(['bob']) ≡ { matched: true, value: [undefined, 'bob'], result: [ { matched: true, value: undefined }, { matched: true, value: 'bob' } ] } matcher(['eve', 'bob']) ≡ { matched: false } matcher(['eve']) ≡ { matched: false }const matcher = matchArray([maybe(group('alice', 'fred')), 'bob']) matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [['alice', 'fred'], 'bob'], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] }, { matched: true, value: 'bob' }, ] } matcher(['bob']) ≡ { matched: true, value: [undefined, 'bob'], result: [ { matched: true, value: undefined }, { matched: true, value: 'bob' } ] }const matcher = matchArray([maybe(some('alice')), 'bob']) matcher(['alice', 'bob']) ≡ { matched: true, value: [['alice'], 'bob'], result: [ { matched: true, value: ['alice'], result: [ { matched: true, value: 'alice' } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'alice', 'bob']) ≡ { matched: true, value: [['alice', 'alice'], 'bob'], result: [ { matched: true, value: ['alice', 'alice'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'alice' } ] }, { matched: true, value: 'bob' } ] } matcher(['bob']) ≡ { matched: true, value: [undefined, 'bob'], result: [ { matched: true, value: undefined }, { matched: true, value: 'bob' } ] } -
notfunction not<T>(unexpected: T): Matcher<T>Matches if value does not match
unexpected. Primitives inunexpectedare wrapped with their correspondingMatcherbuilderExample
const matcher = not(oneOf('alice', 'bob')) matcher('eve') ≡ { matched: true, value: 'eve' } matcher('alice') ≡ { matched: false } matcher('bob') ≡ { matched: false } -
restconst rest: Matcher<any>An
IteratorMatcherthat consumes the remaining elements/properties to prefix matching of arrays and partial matching of objects.Example
const matcher = when( { headers: [ { name: 'cookie', value: defined }, rest ], rest }, ( { headers: [{ value: cookieValue }, restOfHeaders], rest: restOfResponse }, ) => ({ cookieValue, restOfHeaders, restOfResponse }) ) matcher({ status: 200, headers: [ { name: 'cookie', value: 'om' }, { name: 'accept', value: 'everybody' } ] }) ≡ { cookieValue: 'om', restOfHeaders: [{ name: 'accept', value: 'everybody' }], restOfResponse: { status: 200 } } matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false }const matcher = matchArray([42, 'alice', rest]) matcher([42, 'alice']) ≡ { matched: true, value: [42, 'alice', []], result: [ { matched: true, value: 42 }, { matched: true, value: 'alice' }, { matched: true, value: [] } ] } matcher([42, 'alice', true, 69]) ≡ { matched: true, value: [42, 'alice', [true, 69]], result: [ { matched: true, value: 42 }, { matched: true, value: 'alice' }, { matched: true, value: [true, 69] } ] } matcher(['alice', 42]) ≡ { matched: false } matcher([]) ≡ { matched: false } matcher([42]) ≡ { matched: false }const matcher = matchObject({ x: 42, y: 'alice', rest }) matcher({ x: 42, y: 'alice' }) ≡ { matched: true, value: { x: 42, y: 'alice', rest: {} }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, rest: { matched: true, value: {} } } } matcher({ x: 42, y: 'alice', z: true, aa: 69 }) ≡ { matched: true, value: { x: 42, y: 'alice', rest: { z: true, aa: 69 } }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, rest: { matched: true, value: { z: true, aa: 69 } } } } matcher({}) ≡ { matched: false } matcher({ x: 42 }) ≡ { matched: false }const matcher = matchObject({ x: 42, y: 'alice', customRestKey: rest }) matcher({ x: 42, y: 'alice', z: true }) ≡ { matched: true, value: { x: 42, y: 'alice', customRestKey: { z: true } }, result: { x: { matched: true, value: 42 }, y: { matched: true, value: 'alice' }, customRestKey: { matched: true, value: { z: true } } } } -
somefunction some<T>(expected: T): Matcher<T[]>An
IteratorMatcherconsumes all consecutive elements matchingexpectedin the array until it reaches the end or encounters an unmatched element. The next matcher can consume the unmatched element. At least one element must match. Similar to regular expression+operator.somedoes not compose with matchers that consume nothing, such asmaybe. Attempting to compose withmaybewill throw an error as it would otherwise lead to an infinite loop.Example
const matcher = matchArray([some('alice'), 'bob']) matcher(['alice', 'alice', 'bob']) ≡ { matched: true, value: [['alice', 'alice'], 'bob'], result: [ { matched: true, value: ['alice', 'alice'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'alice' } ] }, { matched: true, value: 'bob' } ] } matcher(['eve', 'bob']) ≡ { matched: false } matcher(['bob']) ≡ { matched: false }const matcher = matchArray([some(group('alice', 'fred')), 'bob']) matcher(['alice', 'fred', 'bob']) ≡ { matched: true, value: [[['alice', 'fred']], 'bob'], result: [ { matched: true, value: [['alice', 'fred']], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] } ] }, { matched: true, value: 'bob' } ] } matcher(['alice', 'fred', 'alice', 'fred', 'bob']) ≡ { matched: true, value: [[['alice', 'fred'], ['alice', 'fred']], 'bob'], result: [ { matched: true, value: [['alice', 'fred'], ['alice', 'fred']], result: [ { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] }, { matched: true, value: ['alice', 'fred'], result: [ { matched: true, value: 'alice' }, { matched: true, value: 'fred' } ] } ] }, { matched: true, value: 'bob' } ] } -
allOffunction allOf<T>(expected: ...T): Matcher<T>Matches if all
expectedmatchers are matched. Primitives inexpectedare wrapped with their correspondingMatcherbuilder. Always matches ifexpectedis empty.Example
const isEven = (x) => x % 2 === 0 const matchEven = matchPredicate(isEven) const matcher = allOf(between(1, 10), matchEven) matcher(2) ≡ { matched: true, value: 2, result: [ { matched: true, value: 2 }, { matched: true, value: 2 } ] } matcher(0) ≡ { matched: false } matcher(1) ≡ { matched: false } matcher(12) ≡ { matched: false }const matcher = allOf() matcher(undefined) ≡ { matched: true, value: undefined, result: [] } matcher({ key: 'value' }) ≡ { matched: true, value: { key: 'value' }, result: [] } -
oneOffunction oneOf<T>(expected: ...T): Matcher<T>Matches first
expectedmatcher that matches. Primitives inexpectedare wrapped with their correspondingMatcherbuilder. Always unmatched when emptyexpected. Similar tomatch. Similar to the regular expression|operator.Example
const matcher = oneOf('alice', 'bob') matcher('alice') ≡ { matched: true, value: 'alice' } matcher('bob') ≡ { matched: true, value: 'bob' } matcher('eve') ≡ { matched: false }const matcher = oneOf() matcher(undefined) ≡ { matched: false } matcher({ key: 'value' }) ≡ { matched: false } -
whentype ValueMapper<T, R> = (value: T, matched: Matched<T>) => R function when<T, R>( expected?: T, ...guards: ValueMapper<T, Boolean>, valueMapper: ValueMapper<T, R> ): Matcher<R>Matches if
expectedmatches and satisfies all theguards, then matched value is transformed withvalueMapper.guardsare optional. Primativeexpectedare wrapped with their correspondingMatcherbuilder. Second parameter tovalueMapperis theMatchedResult. SeematchRegExp,matchArray,matchObject,group,someandallOffor extra fields onMatched.Example
const matcher = when( { role: 'teacher', surname: defined }, ({ surname }) => `Good morning ${surname}` ) matcher({ role: 'teacher', surname: 'Wong' }) ≡ { matched: true, value: 'Good morning Wong', result: { role: { matched: true, value: 'teacher' }, surname: { matched: true, value: 'Wong' } } } matcher({ role: 'student' }) ≡ { matched: false }const matcher = when( { role: 'teacher', surname: defined }, ({ surname }) => surname.length === 4, // guard ({ surname }) => `Good morning ${surname}` ) matcher({ role: 'teacher', surname: 'Wong' }) ≡ { matched: true, value: 'Good morning Wong', result: { role: { matched: true, value: 'teacher' }, surname: { matched: true, value: 'Wong' } } } matcher({ role: 'teacher', surname: 'Smith' }) ≡ { matched: false } -
otherwisetype ValueMapper<T, R> = (value: T, matched: Matched<T>) => R function otherwise<T, R>( ..guards: ValueMapper<T, Boolean>, valueMapper: ValueMapper<T, R> ): Matcher<R>Matches if satisfies all the
guards, then value is transformed withvalueMapper.guardsare optional. Second parameter tovalueMapperis theMatchedResult. SeematchRegExp,matchArray,matchObject,group,someandallOffor extra fields onMatched.Example
const matcher = otherwise( ({ surname }) => `Good morning ${surname}` ) matcher({ role: 'teacher', surname: 'Wong' }) ≡ { matched: true, value: 'Good morning Wong' }const matcher = otherwise( ({ surname }) => surname.length === 4, // guard ({ surname }) => `Good morning ${surname}` ) matcher({ role: 'teacher', surname: 'Wong' }) ≡ { matched: true, value: 'Good morning Wong' } matcher({ role: 'teacher', surname: 'Smith' }) ≡ { matched: false }
Matcher consumers
Consumes Matchers to produce a value.
-
matchconst match<T, R>: (value: T) => (...clauses: Matcher<R>) => R | undefinedReturns a matched value for the first clause that matches, or
undefinedif all are unmatched.matchis to be used as a top-level expression and is not composable. To create a matcher composed of clauses useoneOf.Example
function meme(value) { return match (value) ( when (69, () => 'nice'), otherwise (() => 'meh') ) } meme(69) ≡ 'nice' meme(42) ≡ 'meh'function meme(value) { return match (value) ( when (69, () => 'nice') ) } meme(69) ≡ 'nice' meme(42) ≡ undefined const memeMatcher = oneOf ( when (69, () => 'nice') ) memeMatcher(69) ≡ { matched: true, value: 'nice' } memeMatcher(42) ≡ { matched: false }
What about TC39 pattern matching proposal?
patcom does not implement the semantics of TC39 pattern matching proposal. However, patcom was inspired by the TC39 pattern matching proposal and, in-fact, has feature parity. As patcom is a JavaScript library, it cannot introduce any new syntax, but the syntax remains relatively similar.
Comparision of TC39 pattern matching proposal on the left to patcom on the right

Differences
The most notable difference is patcom implemented enumerable object properties matching, whereas TC39 pattern matching proposal implements partial object matching. See tc39/proposal-pattern-matching#243. The rest matcher can be used to achieve partial object matching.
patcom also handles holes in arrays differently. Holes in arrays in TC39 pattern matching proposal will match anything, whereas patcom uses the more literal meaning of undefined as one would expect with holes in arrays defined in standard JavaScript. The any matcher must be explicitly used if one desires to match anything for a specific array position.
Since patcom had to separate the pattern matching from destructuring, enumerable object properties matching is the most sensible. Syntactically separation of the pattern from destructuring is the most significant difference.
TC39 pattern matching proposal when syntax shape
when (
pattern + destructuring
) if guard:
expression
patcom when syntax shape
when (
pattern,
(destructuring) => guard,
(destructuring) => expression
)
patcom offers allOf and oneOf matchers as subsitute for the pattern combinators syntax.
TC39 pattern matching proposal and combinator + or combinator
Note that the usage of and in this example is purely to capture the match and assign it to dir.
when (
['go', dir and ('north' or 'east' or 'south' or 'west')]
):
...use dir
patcom oneOf matcher + destructuring
Assignment to dir separated from the pattern.
when (
['go', oneOf('north', 'east', 'south', 'west')],
([, dir]) =>
...use dir
)
Additional consequence of the separating the pattern from destructuring is patcom has no need for any of:
- interpolation pattern syntax
- custom matcher protocol interpolations syntax
withchaining syntax.
Another difference is TC39 pattern matching proposal caches iterators and object property accesses. This has been implemented in patcom as a different variation of match, which is powered by cachingOneOf.
To see a complete comparison with TC39 pattern matching proposal and unit tests to prove full feature parity, see tc39-proposal-pattern-matching folder.
What about match-iz?
match-iz is similarly inspired by TC39 pattern matching proposal has many similarities to patcom. However, match-iz is not feature complete to TC39 pattern matching proposal, most notably missing is:
- when guards
- caching iterators and object property accesses
match-iz also offers a different match result API, where matched and value are allowed to be functions. The same functionality in patcom can be found in the form of functional mappers.
Contributions welcome
The following is a non-exhaustive list of features that could be implemented in the future:
- more unit testing
- better documentation
- executable examples
- tests that extract and execute samples out of documentation
- richer set of matchers
- as this library exports modules, the size of the npm package does not matter if consumed by a tree shaking bundler. This means matchers of any size will be accepted as long as all matchers can be organized well as a cohesive set
- Date matcher
- Temporal matchers
- Typed array matchers
- Map matcher
- Set matcher
- Intl matchers
- Dom matchers
- other Web API matchers
- eslint
- typescript, either by rewrite or
.d.tsfiles - async matchers
What does patcom mean?
patcom is short for pattern combinator, as patcom is the same concept as parser combinator