matcha_match
v1.0.0
Published
Pattern matching for Typescript and Javascript
Downloads
4
Maintainers
Readme
matcha
Pattern Matching for Typescript and Javascript
matcha provides powerful pattern matching - inspired by f# and functional programming.
Install
npm i matcha_match
...
import { patternMatch, with_ } from 'matcha_match'
import { $string } from 'matcha/runtime-interfaces/$string
Overview
Pattern matching takes a value and matches it against a series of patterns. The first pattern to match, fires the value (with type inferred from the pattern) into an accompanying function.
So... let's say we have name
.
We could do something like...
patternMatch(
name,
with_('garfield', matchedName => `${matchedName} is a cat`)
with_('odie', matchedName => `${matchedName} is a dog`)
)
In the above matchedName
in both cases is inferred to be a string - even though name
may be of unknown type.
That's because matchedName
infers it's type from the pattern.
Pattern Matching can be used to return a value. The result is the result of the function that fires upon match. If there is no match, then the original value is returned instead.
const name: string = getName()
const a = patternMatch(
name,
with_('garfield', matchedName => `${matchedName} is a cat`)
with_('odie', matchedName => `${matchedName} is a dog`)
)
In the above, since the value and both with_
arms all return a string - the compiler is smart enough to know that the resulting type is always string. Therefore a
gets an inferred type of string.
If one of the arms returned a number
then a
would have an inferred type of string | number
.
Literal matching
We've already seen how simple equality matches can be made...
const a = 'cat' as unknown
const b = patternMatch(
a,
with_('cat', _ => `hello kitty`),
with_('dog', _ => `hello doggy`)
)
But Pattern Matching is far more powerful than that...
Partial Matching & Destructuring
Objects and arrays can be matched against a partial object / array.
const a = {
name: {
first: 'johnny',
last: 'bravo'
}
}
patternMatch(
a,
with_({ name: { first: 'johnny '} }, _ => `matching on first name`)
)
Which is particularly useful when used in combination with destructuring
patternMatch(
a,
with_({ name: { first: 'johnny '} }, ({ name: { first: b }}) => `Hey it's ${b}`)
)
Runtime Interfaces
Special runtime interfaces can be used to match against in place of values...
Here we use $string
in place of the literal 'johnny'.
const $matchPattern = {
name: {
first: $string
}
}
patternMatch(
a,
with_($matchedPattern, ({ name: { first: b }}) => `${b} is a string`)
)
It's also good to point out that a runtime interface automatically binds the correct type to the interface, so $string
is of type string
. So when a
is matched, it infers the type { name: { first: string }}
Runtime interfaces are powerful...
const a = [1, 2, 3]
patternMatch(
a,
with_($array($number), a => `${a} is an array of numbers`)
)
patternMatch(
a,
with_([1, $number, 3], ([_, b, __]) => `${b} is a number`)
)
const a = {
a: [1, 2],
b: [3, 3, 4],
c: [1, 5, 99]
}
patternMatch(
a,
with_($record($array($number)), a => `A record of arrays of numbers - whoa`)
)
const a = 'cat' as unknown
console.log(
patternMatch(
a,
with_($lt(100), _ => `< 100`),
with_($gt(100), _ => `> 100`),
with_(100, _ => `its 100`),
with_($unknown, _ => `no idea ... probably a cat`) // Use $unknown as a catch all
)
)
const a = 'cat' as string | number
patternMatch(
a,
with_($union([$string, $number]), _ => `a is string | number`)
)
Runtime interfaces include
$string
$number
$boolean
$array([])
$record()
$union([])
$unknown
$nothing
<- Use this to match on undefined & null$lt
$gt
$lte
$gte
Roll your own Runtime Interfaces
const $even =
{
runtimeInterface: true,
test: (a: number) => a % 2 === 0
} as unknown as number
const $odd =
{
runtimeInterface: true,
test: (a: number) => a % 2 !== 0
} as unknown as number
console.log(
patternMatch(
101,
with_($even, _ => `number is even`),
with_($odd, _ => `number is odd`)
)
) // number is odd
A Runtime interface is an object with the property runtimeInterface: true
.
This tells the with_
function to treat the value as a Runtime Interface.
Primitive Runtime Interfaces have a type
property, but more complex ones have a test
function that determines whether a match is being made.
In both $odd
and $even
the subject is piped into the test function and a boolean is returned which determines whether or not the subject matches.
Note that the Runtime Interface object is coerced into the expected type should the path match.
Simple, Safe Fetch
const $validJson = {
userId: $number,
id: $number,
title: $string,
completed: $boolean
}
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json =>
patternMatch(
json,
match($validJson, json => console.log(`yay - ${ json.title }`)),
match($unknown, a => console.log(`Unexpected JSON response from API`))
)
)
Type-cirtainty
Pattern matching becomes more powerful when used to drive type-cirtainty.
The return value of pattern matching is often a union
type or just plain unknown
.
Instead we can drive type-cirtainty by not returning a response to a variable at all. Instead we call a function passing in the value of cirtain-type from the inferred match.
In the below personProgram
only fires if bob
matches $person
so if personProgram
runs at all, then it is with type-cirtainty.
const $person = {
name: {
first: $string
}
}
type Person = typeof $person
const personProgram = (person: Person) => {
//this program runs with type cirtainty :D
console.log(`${person.name.first} is safe`)
}
const bob = getPerson(123)
patternMatch(
bob,
with_($person, personProgram /* this only runs if a match occurs */),
with_($nothing, _ => console.log('no match'))
)