ts-matcher
TypeScript icon, indicating that this package has built-in type declarations

0.1.4 • Public • Published

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-matcher

or 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(); // 9

The 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(); // 1

Processing 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); // 3

From 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

yarn

Running Tests

yarn test

Building

yarn build

Output 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 perf

The 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.

License

MIT

Readme

Keywords

none

Package Sidebar

Install

npm i ts-matcher

Weekly Downloads

19

Version

0.1.4

License

MIT

Unpacked Size

51.9 kB

Total Files

15

Last publish

Collaborators

  • monnef