Matchblade is a robust TypeScript utility library designed to bring functional programming patterns and type safety to your daily workflow. It provides a suite of powerful tools for pattern matching, data transformation, and async control flow.
npm install matchbladeMatchblade bridges the gap between functional programming concepts and practical TypeScript development.
- Type Safety First: Built with TypeScript in mind, ensuring your data transformations and pattern matching are fully typed.
- Functional Patterns: Brings powerful concepts like pattern matching and piping to standard JavaScript/TypeScript.
- Utility Focused: Solves common problems like deep object traversal, list-to-tree conversion, and async flows without the bloat of larger frameworks.
- Pattern Matching: Expressive
matchandcaseOffor handling complex conditions with type narrowing. - Async Utilities:
pipeAsyncandawaitObjfor managing asynchronous operations cleanly. - Data Structures: Helpers for Trees, Maps, and deep object manipulation.
- Guards:
failOnfor declarative validation.
A powerful pattern matching utility that narrows types based on predicates. match takes a series of caseOf clauses and returns a function that, when called with arguments, will execute the handler of the first matching case.
import { match, caseOf } from 'matchblade';
import { is, _ } from 'ramda';
// Define type guards
const isString = (x: any): x is string => typeof x === 'string';
const isNumber = (x: any): x is number => typeof x === 'number';
// Create a matcher
const myMatcher = match<[string | number, any], string>(
caseOf([isString, _], (str) => `The string is: ${str}`),
caseOf([isNumber, 10], (num) => `The number is 10, and the first arg was ${num}`),
caseOf([isNumber, _], (num) => `It's a number: ${num}`),
caseOf([{ a: 1 }], (obj) => `It's an object with a=1: ${JSON.stringify(obj)}`)
);
// Use the matcher
console.log(myMatcher("hello", 5)); // "The string is: hello"
console.log(myMatcher(42, 10)); // "The number is 10, and the first arg was 42"
console.log(myMatcher(100, "world")); // "It's a number: 100"
console.log(myMatcher({ a: 1, b: 2 }, 0)); // "It's an object with a=1: ..."
// This will throw an error because no case matches
// myMatcher(true, false);Creates a new object by recursively applying transformations to a source object. evolve is a powerful tool for immutably changing object structures. It takes a "spec" object that defines how to transform the source.
- If a spec property is a function and the key exists in the source, the function is called with the source's value for that key.
- If a spec property is a function and the key does not exist in the source, the function is called with the entire source object.
- If a source property is an array of objects and the corresponding spec is an object,
evolvewill be applied to each item in the array. - If a source property is an object and the spec is also an object,
evolvewill be called recursively.
import { evolve } from 'matchblade';
import { inc, map } from 'ramda';
const source = {
a: 1,
b: { c: 2, d: [3, 4] },
e: 'hello',
f: [{ g: 5 }, { g: 6 }]
};
const transformations = {
a: inc, // Increment `a`
b: {
c: (c: number) => c * 2, // Double `b.c`
d: map(inc) // Increment each item in `b.d`
},
newProp: (o: typeof source) => o.a + o.b.c, // Add a new property
f: {
g: inc // Increment `g` in each object in `f`
}
};
const result = evolve(transformations, source);
// result will be:
// {
// a: 2,
// b: { c: 4, d: [4, 5] },
// e: 'hello',
// f: [{ g: 6 }, { g: 7 }],
// newProp: 3 // 1 (original a) + 2 (original b.c)
// }Resolves all Promise values within an object. This function takes an object where some properties are Promises and returns a new Promise that resolves to an object with the same keys, but with all Promises replaced by their resolved values.
import { awaitObj } from 'matchblade';
const data = {
user: Promise.resolve({ id: 1, name: 'Alice' }),
posts: Promise.resolve(['Post 1', 'Post 2']),
version: 2,
};
const resolvedData = await awaitObj(data);
// resolvedData will be:
// {
// user: { id: 1, name: 'Alice' },
// posts: ['Post 1', 'Post 2'],
// version: 2,
// }Creates a function that converts a flat list of objects into a tree structure. You first call it with the configuration (property names for ID, parent ID, and children), and it returns a function that will perform the transformation on a list.
import { listToTree } from 'matchblade';
const nodes = [
{ id: 1, name: 'Root', parent_id: null },
{ id: 2, name: 'Child A', parent_id: 1 },
{ id: 3, name: 'Child B', parent_id: 1 },
{ id: 4, name: 'Grandchild A.1', parent_id: 2 },
];
// Create a tree converter
const toTree = listToTree('id', 'parent_id', 'children');
// Convert the list to a tree
const tree = toTree(nodes);
// tree: { id: 1, name: 'Root', parent_id: null, children: [ ... ] }Creates a Map from a list of elements, where the keys are generated by a provided function and the values are the elements themselves.
import { mapBy } from 'matchblade';
const people = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
];
// Create a map by person's ID
const peopleById = mapBy(person => person.id, people);
// Map { 1 => { id: 1, name: 'Alice' }, 2 => { id: 2, name: 'Bob' }, ... }
console.log(peopleById.get(2)); // { id: 2, name: 'Bob' }Creates an asynchronous pipeline of functions. pipeAsync chains a series of functions, where the output of each function is passed as the input to the next. It seamlessly handles both synchronous and asynchronous functions.
import { pipeAsync } from 'matchblade';
const addOne = (n: number) => n + 1;
const doubleAsync = async (n: number) => {
await new Promise(res => setTimeout(res, 10));
return n * 2;
};
const toString = (n: number) => `Result: ${n}`;
const calculation = pipeAsync(
addOne, // 5 -> 6
doubleAsync, // 6 -> 12 (after a delay)
toString // 12 -> "Result: 12"
);
const result = await calculation(5);
console.log(result); // "Result: 12"Creates a pipeline of functions where each function receives the initial argument and the result of the previous function. pipeTap is useful for creating a sequence of operations that depend on a shared initial state and the outcome of the preceding step.
import { pipeTap } from 'matchblade';
const calculationCorrect = pipeTap(
(initial: number) => initial + 1, // 10 -> 11
(_, prev) => prev + 10, // 11 + 10 = 21
(_, prev) => prev * 2 // 21 * 2 = 42
);
const resultSync = calculationCorrect(10);
console.log(resultSync); // 42Recursively traverses a nested data structure (object or array) and applies a function to each non-container value (i.e., primitives). traverse walks through objects and arrays, and for any value that is not an object or an array, it applies the provided function fn.
import { traverse } from 'matchblade';
const data = {
a: 1,
b: {
c: 2,
d: [3, 4],
},
e: 'hello'
};
// Double all numbers in the structure
const doubler = (val: any) => (typeof val === 'number' ? val * 2 : val);
const result = traverse(doubler, data);
// result will be:
// {
// a: 2,
// b: {
// c: 4,
// d: [6, 8],
// },
// e: 'hello'
// }Creates a function that throws an error if the input matches the guard. This is useful for ensuring at runtime that a value does not conform to a certain type.
import { failOn } from 'matchblade';
// Define a type and a type guard
type Vehicle = 'car' | 'bike' | 'plane';
const isPlane = (v: Vehicle): v is 'plane' => v === 'plane';
// Create a function that will fail if the vehicle is a plane
const rejectPlanes = failOn(isPlane, 'Planes are not allowed!');
try {
const myVehicle = rejectPlanes('car'); // This will pass
console.log(`My vehicle is a ${myVehicle}`); // "My vehicle is a car"
const anotherVehicle = rejectPlanes('plane'); // This will throw an error
} catch (error) {
console.error(error.message); // "Planes are not allowed!"
}© 2023-2025 Andreas Herd
Licensed under the Apache License 2.0: http://www.apache.org/licenses/LICENSE-2.0