https://github.com/attack-monkey/matcha
Pattern Matching for Typescript and Javascript
https://github.com/attack-monkey/matcha
Last synced: 29 days ago
JSON representation
Pattern Matching for Typescript and Javascript
- Host: GitHub
- URL: https://github.com/attack-monkey/matcha
- Owner: attack-monkey
- Created: 2020-09-09T21:43:37.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2020-09-14T19:01:22.000Z (almost 6 years ago)
- Last Synced: 2025-01-25T08:44:03.558Z (over 1 year ago)
- Language: JavaScript
- Size: 36.1 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
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...
```typescript
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.
```typescript
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...
```typescript
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.
```typescript
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
```typescript
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'.
```typescript
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...
```typescript
const a = [1, 2, 3]
patternMatch(
a,
with_($array($number), a => `${a} is an array of numbers`)
)
```
```typescript
patternMatch(
a,
with_([1, $number, 3], ([_, b, __]) => `${b} is a number`)
)
```
```typescript
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`)
)
```
```typescript
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
)
)
```
```typescript
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
```typescript
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
```typescript
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.
```typescript
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'))
)
```