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");