Skip to content

Dkendal/match-ts

Repository files navigation

@dkendal/match

Provides pattern matching features typically found in functional languages like Elixir/Erlang, ML, F#, etc.

Installation

yarn add @dkendal/match

The match function

The match function lets us compare a value against many patterns until we find one that matches.

import { __, match, when } from '@dkendal/match'

match(
  [1, 2, 3],
  when([4, 5, 6], () => "This clause won't match"),
  when([1, __, 3], () => 'This clause will match, __ will match any value'),
  when(__, () => 'This clause would match any value'),
)

'This clause will match, __ will match any value'

The value that was passed into match is also passed to the callback:

const x = 10

match(
  x,
  when(__, (value) => value * 10),
)
100

If none of the cases match and error is thrown:

match([1, 2, 3]
      when([1, 2], () => "this won't match as the array lengths don't agree"))
// (Error) unmatched case: [1, 2, 3]

The when function

The when function exists purely for typing. When you use it gives the callback the proper type expressed in the left-hand-side pattern:

when(
  V('checkout', {
    type: 'checkout',
    lineItems: [V('firstLineItem', { type: 'line_item', data: __ }), __.rest],
  }),
  ({ checkout, firstLineItem }) => {
    var checkout: {
      type: 'checkout'
      id: number
      lineItems: [{
        type: 'line_item'
        id: number
        data: any
      }, any[]]
    }

    var firstLineItem: {
      type: 'line_item'
      id: number
      data: any
    }
  },
)

match function continued

Array's are matched as tuples, the lengths have to be the same:

match([1, 2, 3]
      when([1, 2], () => "this won't match"),
      when([1, 2, 3], () => "this will match"))
"this will match"

The tail of a list can be matched with __.rest, or it's aliases __.tail or __.tl:

match(
  range(50, 100),
  when([50, 51, __.rest], () => 'this will match'),
)
;('this will match')

Primitive types can be matched with special operators:

const thingy: {
  data: {
    someNumber: 1
    metadata: { foobar: 'foobar' }
    someOtherProp: {}
  }
  list: [1, 2, 3]
}

match(
  thingy,
  when(
    {
      data: { someNumber: __.number, metadata: __.object },
      list: [__, __.rest],
    },
    () => 'this will match',
  ),
)
;('this will match')

You can capture variables and use them on the right hand side of match, if you use when the callback will be typed with all variable bindings:

const thingy: {
  meta: {
    type: 'contrived example',
    x: 1,
    y: 2,
  },
  data: {
    a: {
      b: {
        c: {
          d: {
            message: 'hello world'
            value: 42
          }
        }
      }
    }
  }
}

match(thingy,
      when({ meta: V('x'),
         data: { a: { b: { c: { d: { value: V('y') } } } } }
       }, (captures) => captures))
{
  x: {
    type: 'contrived example',
    x: 1,
    x: 2,
  }
  y: 42
}

Variable captures can also apply a pattern that must also match for the case.

import { match, __, V } from "pattern-matching"

const checkout = {
  object: "list",
  data: [
    {
      id: "li_1",
      object: "item",
      ...
    },
    {...},
    {...}
  ]
}
match(value,
      when( { object: 'list',
          data: V('data')([
            V('first')({ object: 'item' }),
            __.rest
          ])
        }, (captures) => captures ))

{
  data: [
    {
      id: "li_1",
      object: "item",
      ...
    },
    {...},
    {...}
  ]
  first: {
    id: "li_1",
    object: "item",
    ...
  },
}

TODOs

  • Proper type inference right-hand-side of match case
  • Exhaustive type check for match cases