Skip to content

Exploring Pattern Matching in TypeScript

What is Pattern Matching?

Pattern Matching is a declarative much more powerful and less verbose alternative to imperatives “if/else” conditions.

A definition can be found inside Scala Documentation

“Pattern matching tests whether a given value (or sequence of values) has the shape defined by a pattern, and, if it does, binds the variables in the pattern to the corresponding components of the value (or sequence of values).”


In Functional Programming languages, there're built-in keywords for Pattern Matching. Typescript though is one language that works very well with Functional Programming but lacks this feature, for this reason I made a package pattern-matching-ts that aims to bring Pattern Matching feature to Typescript through Discriminated Union Types / Algebraic Data Types.

Pattern Matching with Option

What’s an Option Monad?

In programming languages (more so functional programming languages) and type theory, an option type or maybe type is a polymorphic type that represents an encapsulation of an optional value; e.g., it is used as the return type of functions which may or may not return a meaningful value when they are applied. It consists of a constructor which either is empty (often named None or Nothing), or which encapsulates the original data type A (often written Just A or Some A). >


Let's implement our Option Type signature

interface None {
  readonly _tag: "None";
}

interface Some<A> {
  readonly _tag: "Some";
  readonly value: A;
}

type Option<A> = None | Some<A>;

Now that we have defined the Option type signature we can use it as a discriminated union for our pattern matching.

The pattern-matching package

yarn

yarn add pattern-matching-ts

npm

npm install --save pattern-matching-ts

Now we are ready to implement our option pattern matching.

 * as M from 'pattern-matching-ts/lib/match'


const optionMatching = M.match<Option<string>, string>({
  Some: (x) => `Some: ${x.value}`,
  None: () => 'Nothing'
})

assert.deepStrictEqual(
  optionMatching(O.some('data')),
   'Some: data'
)

Let’s say we have to handle more cases than a simple value that may or not be there…

we can achieve that easily by defining a discriminated union

interface ChangeColor<T = number> {
  readonly _tag: "ChangeColor";
  readonly value: {
    readonly r: T;
    readonly g: T;
    readonly b: T;
  };
}
interface Move<T = number> {
  readonly _tag: "Move";
  readonly value: {
    readonly x: T;
    readonly y: T;
  };
}

interface Write {
  readonly _tag: "Write";
  readonly value: {
    readonly text: string;
  };
}

type Cases = ChangeColor<number> | Move | Write;

Now we are ready to build our Pattern Matching by implementing all the cases plus the required default that uses _: as a reserved keyword.

import * as M from "pattern-matching-ts";

const matchMessage = M.match<Cases, string>({
  ChangeColor: ({ value: { r, g, b } }) =>
    `Red: ${r} | Green: ${g} | Blue: ${b}`,
  Move: ({ value: { x, y } }) =>
    `Move in the x direction: ${x} and in the y direction: ${y}`,
  Write: ({ value: { text } }) => `Text message: ${text}`,
  _: () => "Default message",
});

const ChangeColor = ({ r, g, b }: ChangeColor<number>["value"]) => ({
  _tag: "ChangeColor",
  value: { r, g, b },
});

assert.deepStrictEqual(
  matchMessage(ChangeColor({ r: 12, g: 20, b: 30 })),
  "Red: 12 | Green: 20 | Blue: 30"
);

assert.deepStrictEqual(matchMessage(null), "Default message");

pattern-matching-ts source-code

pattern-matching-ts NPM