Skip to content

attack-monkey/matcha

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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'))
)

About

Pattern Matching for Typescript and Javascript

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published