Type-safe expression matching. Similar to PHP's match expression.
npm install matchee
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.
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
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
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
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
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
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"
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
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.
}
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
}
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!
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
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
All ideias and suggestions are welcome. Just create an issue or a pull request. Current not implemented features can be found here.