DEV Community

Cover image for Pipeable Pattern Matching in Typescript
Stefano Regosa
Stefano Regosa

Posted on • Updated on

Pipeable Pattern Matching in Typescript

In the latest post, we talked about pattern matching
Match API.

In this article we gonna look into the matchW API, a wider match implementation where the match happens within a pipe function.

A pipe is a function of functions that pipes the value of an expression into a pipeline of functions.
This function takes an initial value and passes that as the argument(s) for the first internal function to use, then it takes the result from that function and passes it to another internal function.


A pipe typescript implementation can be found inside the fp-ts library so we need to install that library first.

yarn

yarn add fp-ts 
Enter fullscreen mode Exit fullscreen mode

npm

npm install --save fp-ts 
Enter fullscreen mode Exit fullscreen mode

MatchW

With this api we don't need to specify via generic the matches or return type since both signatures are inferred within the pipe function.
The only thing we have to specify in order to implement our pattern matching function is the discriminated union.

Let's implement our matchW pattern-matching against the fp-ts Option.

If we look at the fp-ts Option implementation

export interface None {
  readonly _tag: 'None'
}

export interface Some<A> {
  readonly _tag: 'Some'
  readonly value: A
}

export type Option<A> = None | Some<A>
Enter fullscreen mode Exit fullscreen mode

we see that the convention used was _tag, so we have to specify that inside our implementation as well.

M.matchW('_tag')
Enter fullscreen mode Exit fullscreen mode

Once we did that, the LSP auto-complete will do the rest by providing suggestions via Intellisense:

for each case
alt MatchW

and each implementation
alt MatchW

Option MatchW

import * as M from 'pattern-matching-ts/lib/match'
import { pipe } from 'fp-ts/lib/function'
import * as O from 'fp-ts/lib/Option'

const optionMatching = (o: O.Option<string>) =>
  pipe(
    o,
    M.matchW('_tag')({
      Some: ({ value }) => 'Something: ' + value,
      None: () => 'Nothing',
    })
  )

assert.deepStrictEqual(optionMatching(O.some('data')), 'Something: data')
assert.deepStrictEqual(optionMatching(O.none), 'Nothing')
Enter fullscreen mode Exit fullscreen mode

Following the previous example, we can implement the pattern-matching against the Either.

Either MatchW

import * as M from 'pattern-matching-ts/lib/match'
import { pipe } from 'fp-ts/lib/function'
import * as E from 'fp-ts/lib/Either'

type RGB = Record<'r' | 'g' | 'b', number>
const either = (maybeRgb: E.Either<string, RGB>) =>
  pipe(
    maybeRgb,
    M.matchW('_tag')({
      Left: ({ left }) => 'Error: ' + left,
      Right: ({ right: { r, g, b } }) => `Red: ${r} | Green: ${g} | Blue: ${b}`
    })
  )

assert.deepStrictEqual(either(E.right({ r: 255, g: 255, b: 0 })), 'Red: 255 | Green: 255 | Blue: 0')
Enter fullscreen mode Exit fullscreen mode

Since the matchW discriminated union can extend also number let's see how we can implement a pattern-matching against HTTP codes

import * as M from 'pattern-matching-ts/lib/match'
import { pipe } from 'fp-ts/lib/function'

interface ServerResponse<Code extends string | number> {
  readonly code: Code
}

interface Response<Body> {
  readonly response: {
    readonly body: Body
  }
}

interface Success extends ServerResponse<200>, Response<ReadonlyArray<string>> {}

interface NotFoundError extends ServerResponse<404> {}

interface ServerError extends ServerResponse<500> {
  readonly detail: string
}

type Responses = Success | NotFoundError | ServerError

const matchResponse = (response: Responses) =>
  pipe(
    response,
    M.matchW('code')({
      500: ({ detail }) => ({ message: 'Internal server error', detail }),
      404: () => ({ message: 'The page cannot be found!' }),
      200: ({ response }) => response.body,
      _: () => 'Unexpected response'
    })
  )

assert.deepStrictEqual(either(E.right({ r: 255, g: 255, b: 0 })), 'Red: 255 | Green: 255 | Blue: 0')
assert.deepStrictEqual(matchResponse({ code: 200, response: { body: ['data'] } }), ['data'])
assert.deepStrictEqual(matchResponse({ code: 500, detail: 'Cannot connect to the database' }), {
  message: 'Internal server error',
  detail: 'Cannot connect to the database'
})
assert.deepStrictEqual(matchResponse({ code: 404 }), { message: 'The page cannot be found!' })
Enter fullscreen mode Exit fullscreen mode

pattern-matching-ts source-code

pattern-matching-ts NPM

Top comments (4)

Collapse
 
carpenterskeys profile image
Wheelwright • Edited

Does fp-ts have an idiomatic way of handling pattern matching for objects by now?

I was hoping to find a module for Match, or a module with something like Object.matchW('someProp')

I saw the Record module but it doesn't have match.

Collapse
 
waynevanson profile image
Wayne Van Son • Edited

What's the feasability of a signature like this? Without the need for pipe. Don't know how to keep track of K. Only way is to ensure that the Option generic contains only 1 related value at compile time so that the user only inputs one possible string at runtime.

const matchOption = matchW<Option<string>>("_tag")({
  Some: ({ value }) => `Hello, ${value}`,
  None: () => `Goodnight`
}) 
Enter fullscreen mode Exit fullscreen mode

Or alternatively zoom in on sum types that use the tag name as a value? This one seems hard. Means we don't have to destructure the argument.

type Sided<A, B> =
  | { _tag: "SideA", SideA: A }
  | { _tag: "SideB", SideB: B }

export const matchEither = matchW<Sided<string, number>>({
  // `value` is string
  SideA: value => `Side A: ${value}`
  // `value` is number
  SideB: value => 42 + value 
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
skona27 profile image
Jakub Skoneczny

I don't quite get this part:

const optionMatching = (o: O.Option<string>) =>
  pipe(
    o,
    M.matchW('_tag')({
      Some: ({ value }) => 'Something: ' + value,
      None: () => 'Nothing',
    })
  )
Enter fullscreen mode Exit fullscreen mode

Why do we pass o through a pipe? It is not a function because later in assertion, we provide a O.some('data') as a parameter for optionMathing.

Collapse
 
stefano_regosa profile image
Stefano Regosa

you are right in fact it isn't !!!
if we look at the fp-ts pipe signature and all the types overload ... the first params is a Generic and doesn't have to be a function :

gcanti.github.io/fp-ts/modules/fun...

for this reason, we can pass the Option
gcanti.github.io/fp-ts/modules/Opt... as the first argument and then pipe its model inside the M.matchW('_tag')