Pattern matching for TypeScript
One of widely used functional programming feature is pattern matching. Unfortunately there is no language support for it yet in TypeScript - that’s when libraries comes to rescue and this is my take on it.
TypeMatcher
TypeMatcher is a tiny javascript library designed to provide basic pattern matching constructs and in this post I’ll describe how it works and give few usage examples.
WARNING: Post updated for version 0.9.2, breaking changes may happen in version with minor updates until version 1.x.
Library consists of two main components:
Matchers
Matcher is a function which checks that input value matches representing type:
Some provided matchers are: isString
, isNumber
, isArrayOf
, isTuple1
.
TypeMatcher type is compatible with lodash functions with
same purpose
so you can use them directly.
Matching DSL
Matching DSL consists of match
, caseWhen
and caseDefault
functions, used to build case handlers and evaluate match expression over a given value.
match(value, cases)
takes an input value, a case handler with type type MatchCase<A, R> = { map: (val: A) => R }
,
and returns first matching result or throws an error if none matched.
caseWhen(matcher, fn)
is used to build MatchCase<A, R>
instances, using TypeMatcher<T>
and handler functions.
To handle default case - use caseDefault()
function or method.
Installation
npm install --save [email protected]
Examples
Match exact values
Ensure input matches defined enum values:
Exhaustivity checking
Compiler (tsc
) will succeed when cases do cover all possible input values:
But when we remove one of the cases, ex:
compiler will fail:
Match object fields
Ensure input object has all fields defined by your custom type:
Match arrays
Ensure all array values match given type:
Match tuples
Library defines matchers for isTuple1
to isTuple10
, this ought to be enough for anybody :).
Custom matchers
You can provide you own matcher implementations compatible with TypeMatcher<T>
type:
Pre-build matcher
Basically, match expression is evaluation of a function A => B
, where A
is our input value and B
is an union type of cases result types, and we can build this function using matcher
builder, which is also useful for highly performance-sensitive code and you want to save some CPU cycles:
Check source code for all defined matchers: https://github.com/lostintime/node-typematcher/blob/master/src/lib/matchers.ts.
Limitations
Case handlers type variance
Avoid explicitly setting argument type in caseWhen()
handler function, let type inferred by compiler.
You may set more specific type, but check will bring you more general one and compiler will not fail.
This is caused by TypeScript Function Parameter Bivariance
feature.
UPD: Typescript v2.6 brings --strictFunctionTypes
compiler option and if it’s on, for this code:
you will now get this error:
Use caseDefault
at the end
match
will execute all cases as provided, so first matching will return,
use caseDefault
, caseAny
last.
New match DSL introduced in [email protected]
brought compile-time exhaustivity checking, so this code:
will fail at compile time with:
But you still have to handle default case when any
result type is expected (which is highly NOT recommended), otherwise it may fail with No match
error at runtime.
Links
- TypeMatcher source code
- TypeMatcher npm package
- Pattern Matching Support - TypeScript language support proposal
- ECMAScript Pattern Matching Syntax - ES language support proposal
- Support some non-structural (nominal) type matching in TypeScript