Deep Match
Overview
Sometimes it is useful in TypeScript to have have a complex condition on a (nested) object. This can lead to long if
statements that are also hard to read.
type Person = {
name: string;
age: number;
address: {
street: string;
city: string;
};
};
function process(person: Person) {
if (
person.age < 18 &&
person.name === 'Bart' &&
person.address.city === 'Springfield'
) {
console.log('you shall pass');
}
}
The match
function offers, in a similar fashion to patch
, a way to concisely define the conditions an (immutable) object should meet:
import { Deep } from '@rimbu/core';
function process(person: Person) {
if (
Deep.match(person, {
age: (v) => v < 18,
name: 'Bart',
address: {
city: 'Springfield',
},
})
) {
console.log('you shall pass');
}
}
Example sandbox
The following CodeSandbox shows in more detail how match
can be used for more complex use cases:
Details for the match methods
The Rimbu Match Notation offers a flexible way to match many conditions on values in one go. The match functions evaluate the given conditions and return either true if all the conditions are met, or false otherwise.
The match notation has similarities to the patch
notation, but is a bit more relaxed in terms of requirements.
This section goes into more details about the match notation.
Match simple values
To match simple values, all that is needed is to provide a value to compare to.
match(1, 1); // -> true
match('a', 'b'); // -> false
match(true, false); // -> false
Match objects
To match objects, the following options are available:
- partial match, will match all the given property values to the corresponding matchers
- compound match, an array starting with the compound type, and a number of matchers to test
// partial first-level match
match({ a: 1, b: { c: 'q' } }, { a: 1 }); // -> true
// partial deep match
match({ a: 1, b: { c: 'q' } }, { b: { c: 'z' } }); // -> false
// compound match
match(
{
a: 1,
b: { c: 'q' },
},
['some', { a: (v) => v > 5 }, { b: { c: 'q' } }]
); // -> true, the second item matches
Arrays and Tuples
To match arrays and tuples, the following options are available:
- provide an array of matchers that will match each element at the corresponding index
- provide an object with matchers for specific indices
- traversal matches to apply to each element in the array
- compound matches for the entire array wrapped in an object
const arr: number[] = [1, 2, 3];
match(arr, [1, 2, 3]); // -> true, each element matches
match(arr, { 1: 10 }); // -> false, element at index 1 is not 10
const tup = [1, true, 'a'] as [number, boolean, string];
match(tup, [1, false, 'a']); // -> false, element 1 does not match
match(tup, { 0: 1, 2: 'a' }); // -> true, elements at index 0 and 2 match
// traversal matches
match(arr, { someItem: 2 }); // -> true, 2 matches an element is in the array
match(arr, { everyItem: (v) => v < 3 }); // -> false, last item does not match
// compound matches
match(
[
{ x: 1, y: 2 },
{ x: 3, y: 0 },
],
{ some: [{ someItem: { x: 2 } }, { someItem: { y: 0 } }] }
); // -> true, matches the second condition, because there is an item in the array with y = 0
// explained fully, this last match looks for some element in the array where x = 2 or y = 0
Function matchers
At all places where a match is provided, it is also allowed to provide a function returning either a value to match, or a boolean indicating the result of a custom match.
The matcher function receives three arguments:
- value: the value of the item to match
- parent: if nested in an object or array, this is the item one level up, otherwise, will be equal to
value
. - root: if nested in an object or array, this is the root object provided to the patch function, otherwise, will be equal to
value
.
match(1, (v) => v > 0); // => true
match(1, (v) => v * 2 - 1); // => true
match(
{
a: 1,
b: { c: 2, d: { e: 3 } },
},
{
b: {
c: (value, parent, root) => value === parent.d.e - root.a,
},
}
); // => true
// instead of returning the boolean in the previous statement, it is also possible to directly match the value
match(
{
a: 1,
b: { c: 2, d: { e: 3 } },
},
{
b: {
c: (value, parent, root) => parent.d.e - root.a,
},
}
); // => true
Caveat
In the case of matching booleans with functions, there is a conflict between the ability to match the value returned by a function, and the ability to return a boolean for the match result.
In this case, the return value will be considered to indicate whether the match succeeded.
match(false, false); // -> true
match(false, () => false); // -> false, function indicates match result
Match by reference
By default the match functions will inspect value contents when possible. However, sometimes it is needed to match values by reference. The easiest way to achieve this is to provide a function doing the reference check and returning a boolean.
const l = List.of(1, 2, 3);
// this will traverse the entire object tree, probably not desired
match(l, l);
// match by reference
match(l, (v) => v === l);