ts-matcher v0.1.4
TSMatcher
What is it?
It is a small library which improves switch statement from JavaScript/TypeScript.
Why?
I am spoiled by advanced match in Scala which can match deeply, on more cases at once or use guards.
This library strives to improve very basic switch statement, closing the gap a bit between TypeScript and Scala (and other languages with powerful matching, like Haskell).
Show me code!
Let's implement a basic calculator:
import Match from 'ts-matcher';
type Operation = '+' | '-' | '*' | '/';
interface Computation {
a: number;
b: number;
op: Operation;
result: number;
}
// using TSMatcher library
const compute = (a: number, b: number, op: Operation): Computation =>
Match(op)
.case('+', () => a + b)
.case('-', () => a - b)
.case('*', () => a * b)
.case('/', () => a / b)
.execMap(result => ({a, b, op, result}));
// using plain old switch statement
const computeSwitch = (a: number, b: number, op: Operation): Computation => {
let result;
switch (op) {
case '+':
result = a + b;
break;
case '-':
result = a - b;
break;
case '*':
result = a * b;
break;
case '/':
result = a / b;
break;
}
return {a, b, op, result: <number>result};
};
compute(1, 2, '+'); // {a: 1, b: 2, op: '+', result: 3}You can see that in many cases TSMatcher is more concise, yet more powerful, than the built-in switch.
Example above mainly demonstrates an ability to use Matcher as an expression which is very common in functional languages.
For more information read the features section.
Installation
You can use npm
npm i -S ts-matcheror grab a compiled version from this repository in /dist/src directory.
Basic usage
Create a matcher similarly to how one writes a switch:
const animal = 'dog';
Match(animal)then add cases:
.case('spider', () => console.log('I don\'t like those.'))
.case('dog', () => console.log('What a good boy!'))and finally, don't forget to execute the matcher:
.exec();You should see a result of out little program printed out:
What a good boy!If no case is successful an exception is thrown.
Usually we use default to handle unmatched values.
Match(2)
.case(0, () => 0)
.case(1, () => 1)
.default(() => 9)
.exec(); // 9The animal example could be further simplified by using execMap which allows us to do side-effects with the result (or to apply transformations):
Match(animal)
.case('spider', () => `I don't like those.`)
.case('dog', () => 'What a good boy!')
.execMap(x => console.log(x));Features
For more complete examples of usage please look at tests.
Short-circuit evaluation of cases
Only first successfully matched case will get evaluated.
Match(true)
.case(true, () => console.log(0))
.case(true, () => console.log(1))
.exec(); // only prints "0"Deep equality
By default an equality check of the case is deep.
interface AB {a: { b: number }}
const obj: AB = {a: {b: 5}};
Match(obj)
.case({a: {b: 4}}, () => 'a')
.case({a: {b: 5}}, () => 'b')
.exec(); // 'b'Guards
You can also "match" against a function. This usage is very close to multiple if statements chained by elsees.
Nice thing is that you can mix classic case with conditional one caseGuarded.
Match(-5)
.caseGuarded(x => x < 0, () => 'less')
.case(0, () => 'zero')
.caseGuarded(x => x > 0, () => 'more')
.exec(); // 'less'Comparison to multiple values
In some languages, like Scala, one can have multiple values in one case.
With TSMatcher you can compare to multiple values too:
Match('c')
.caseMulti(['a', 'd', 'e'], () => 2)
.caseMulti(['b', 'c'], () => 1)
.exec(); // 1Processing result
To process (aka map) a result you can use the execMap chain-terminating method instead of exec.
Match(1)
.case(0, () => 'bb')
.default(() => 'ccc')
.execMap(x => x.length); // 3From case handler is returned 'ccc', then a function in execMap is called (passing it the 'ccc') and its result 3 is returned.
Equality checking
If package lodash.isequalwith is present, then this function is used.
Otherwise === operator will be utilized.
customizer can be passed to case and caseMulti to customize behaviour of equality checking.
You can change equality checking like this:
import Match, { EqualityChecker } from 'ts-matcher';
// not well typed
const customEqualityFunction = (a: number, b: number, customizer: number) => a + customizer === b;
EqualityChecker.initialize(customEqualityFunction);
Match(1)
.case(3, () => '1=3', 2)
.exec(); // '1=3'Development
Installing Dependencies
yarnRunning Tests
yarn testBuilding
yarn buildOutput is located in dist/src directory.
Drawbacks
Performance
As it is with majority of abstractions, it comes with a performance cost. If you require extremely high performance and/or are not willing to make a trade-off for better abstractions, then I don't recommend using this library. Please note that unless you plan on using it in a very tight loop (e.g. real-time rendering, computing animation in every frame, game loop or intensive data processing) then you are probably fine, since even with only 1ms of work inside switch/matcher impact of this library is for practical purposes non-existent (exactly same ops/s).
You can try yourself:
yarn run perfThe library could be improved to support "prepared" matcher objects, but at this time I have no need for it (I primarily write code for ordinary front-ends and this kind of performance is rarely needed).
Loss of type narrowing
If you rely in all your switches on type narrowing (tagged unions), then this library is not for you.
I might look into it in future, but I am not sure if it is even possible.