Skip to content

gabrieljmj/matchee

Repository files navigation

matchee

npm version build padding

Type-safe expression matching. Similar to PHP's match expression.

Installation

npm install matchee

Motivation

The match function is a type-safe way to match expressions. It is similar to the switch statement, but with a more concise syntax and more flexibility. It allows to match more than one expression at a time and more types of expressions.

Currently on JavaScript, the switch or object literals statement are the most common way to match expressions. However, it is not type-safe and it is not possible to match more than one expression at a time.

Also, it is possible to pass functions as values. This allows to make heavy computations or database queries or any other side effect only when needed.

Usage

The basic usage is to pass an array of cases to the match function. Each case is an array of values, where the last value is the result of the match.

It is possible to pass a default case, which is a single value. If no case matches, the default case is returned.

import { match } from 'matchee';

const matcher = match([
  [1, 2, '100'],
  [3, '200'],
  '300', // default
]);

matcher(1); // 100
matcher(2); // 100
matcher(3); // 200
matcher(5); // 300

Using functions as values

When the values are functions, they are called only when the case matches.

import { match } from 'matchee';

const matcher = match([
  [1, () => '100'],
  [2, () => '200'],
  [3, '300'],
  '400', // default
]);

matcher(1); // 100
matcher(2); // 200
matcher(3); // 300
matcher(5); // 400

For promises

When some of the values are functions that return promises, use the asyncMatch function instead.

import { asyncMatch } from 'matchee';

const matcher = asyncMatch([
  [1, () => Promise.resolve('100')],
  [2, () => '200'],
  [3, '300'],
  '400', // default
]);

await matcher(1); // 100
await matcher(2); // 200
await matcher(3); // 300

Using objects paths

A special syntax is available to match objects paths. It is possible to use the helper objectPaths create expressions to match object values.

import { match, objectPaths } from 'matchee';

const matcher = match([
  [
    objectPaths({
      'user.role': 'admin',
    }),
    'ADMIN_ROLE',
  ],
  [
    objectPaths({
      'user.role': 'user',
    }),
    'USER_ROLE',
  ],
  'GUEST_ROLE', // default
]);

matcher({
  user: {
    role: 'admin',
  },
}); // ADMIN_ROLE
matcher({
  user: {
    role: 'user',
  },
}); // USER_ROLE
matcher({
  user: {
    role: 'guest',
  },
}); // GUEST_ROLE

Using arrays

Arrays are objects, which means that it is possible to use the objectPaths helper to match array values, since indexes are properties.

import { match, objectPaths } from 'matchee';

const matcher = match([
  [
    objectPaths({
      '0.1': 'foo',
    }),
    'bar',
  ],
]);

matcher([['foo', 'baz']]); // bar

Usage tricks

Using boolean values

Using the same example used on PHP docs, we can use the match to check for boolean values. The first match case will be used.

import { match } from 'matchee';

const age = 23;

const matcher = match([
  [age >= 65, 'senior'],
  [age >= 18, 'adult'],
  [age >= 13, 'teenager'],
  'kid',
]);

const result = matcher(true); // "adult"

Using regular expressions

import { match } from 'matchee';

const regex = /foo|bar|baz/;
const matcher = match([[regex, 'match'], 'no match']);

matcher('foo'); // "match"
matcher('bar'); // "match"
matcher('baz'); // "match"
matcher('qux'); // "no match"

or a more complex example using brazilian document numbers:

import { match } from 'matchee';

const cpfRegex = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/;
const cnpjRegex = /^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/;

const matcher = match([
  [cpfRegex, 'CPF'],
  [cnpjRegex, 'CNPJ'],
  () => {
    throw new Error('Invalid document');
  },
]);

matcher('123.456.789-10'); // "CPF"
matcher('12.345.678/9012-34'); // "CNPJ"
matcher('invalid'); // Error: Invalid document

No matches found

If no match is found and no default case is provided, an error is thrown.

import { match } from 'matchee';

try {
  const matcher = match([
    [1, 2, '100'],
    [3, '200'],
  ]);

  matcher(4);
} catch (error) {
  console.log(error.message); // UnhandledMatchExpression: No matching expression found for value 4. Maybe try adding a default value.
}

Checking if an error is from matchee

There is a helper function to check if an error is an UnhandledMatchExpression error: isMatchError.

import { match, isMatchingError } from 'matchee';

try {
  // something that might throw an error...

  const matcher = match([
    [1, 2, '100'],
    [3, '200'],
  ]);

  matcher(4);
} catch (error) {
  if (isMatchingError(error)) {
    // handle match error

    return;
  }

  // handle other errors
}

Specifying type of conditions and values

The match function accepts generics to specify both condition and value types. The first one is the type of the conditions and the second one is the type of the values.

import { match } from 'matchee';

match<number, string>([
  [1, 2, '100'],
  ['3', '200'], // ts-error: Type 'string' is not assignable to type 'number'.
  '300', // default
]);

match<number | string, string>([
  [1, 2, '100'],
  ['3', '200'],
  '300', // default
]); // works!

Inferring result type

It is provided a type-safe way to infer the result type of the match expression. The InferMatchCondition type is used to infer the result type.

import { match, type InferMatchCondition } from 'matchee';

const matcher = match([[1, 2, '100'], [3, '200'], '300']);

type ResultType = InferMatchCondition<typeof matcher>; // string

Available expression types

It is possible to use any type of expression as a match case. The following types are supported:

  • boolean
  • number
  • string
  • object
  • Symbol
  • RegExp
  • ObjectPaths - a special type to match object paths

Contributing

All ideias and suggestions are welcome. Just create an issue or a pull request. Current not implemented features can be found here.