fn
A utility library for TypeScript.
This package composes several libraries into one:
- @monstermann/match - Zero-runtime exhaustive pattern matching.
- @monstermann/dfdl - Data-First / Data-Last oriented utilities.
- @monstermann/remmi - Reverse immer.
- @monstermann/array - Functional utilities for arrays.
- @monstermann/function - Functional utilities.
- @monstermann/geometry - Functional geometry utilities.
- @monstermann/map - Functional utilities for maps.
- @monstermann/number - Functional utilities for numbers.
- @monstermann/object - Functional utilities for objects.
- @monstermann/promise - Functional utilities for promises.
- @monstermann/result - Functional utilities for success | error types.
- @monstermann/set - Functional utilities for sets.
- @monstermann/string - Functional utilities for strings.
- @monstermann/dll - Functional doubly-linked lists.
- @monstermann/dsp - Small & fast disposables.
Features
- Opt-in mutability
- Reference preservation
- Pipe-friendly
- Graceful failure handling
- Namespaces
- Native aliases
- Tree-shaking
Opt-in mutability
All transforming functions treat data as immutable by default, but additionally feature an implementation that clones data on first write, then mutates.
You can read more about how this works in the documentation for remmi, which is what is used under the hood here!
copymutationconst a = [];
const b = Array.append(a, 0);
const c = Array.append(b, 1);
const d = Array.append(c, 2);withMutations(() => {
const a = [];
const b = Array.append(a, 0);
const c = Array.append(b, 1);
const d = Array.append(c, 2);
return d;
});Reference preservation
Most transforming functions return the original target when nothing changed.
This is particularly crucial in front-ends, as the primary means to prevent unnecessary and expensive work from happening is to compare state, whether you are using signals with Vue, or React in tandem with the React Compiler.
const before = { foo: true, bar: false };
const after = Object.merge(before, { foo: true });
before === after; // trueconst before = [1, 2, 3];
const after = Array.filter(before, (value) => value > 0);
before === after; // trueExample implementation
const before = [1, 2, 3];
const after = mapEach(before, (value) => value);
before === after; // true// Short and simple, but inefficient:
function mapEach(before, mapper) {
const after = before.map(mapper);
return isShallowEqual(before, after) ? before : after;
}function mapEach(array, mapper) {
let result;
for (let i = 0; i < array.length; i++) {
const prev = array[i];
const next = mapper(prev, i, array);
// Skip if nothing changed:
if (is(prev, next)) continue;
// Lazily clone the array if we haven't done so already:
result ??= [...array];
// Mutate the clone:
result[i] = next;
}
// `result` is undefined if nothing changed:
return result ?? array;
}Pipe-friendly
Functions support both "data-first" and "data-last" signatures for seamless use with pipe and other methods typically found in functional programming styles.
Array.findRemove([1, 2, 3, 4], (x) => x > 2); // [1, 2, 4]pipe(
[1, 2, 3, 4],
Array.findRemove((x) => x > 2),
); // [1, 2, 4]Graceful failure handling
Most functions silently ignore potentially failing or ambiguous cases by returning sensible fallback values - no hidden exceptions!
Instead, potentially failing utilities come with or, orElse and orThrow variants:
Array.first([]); // undefined
String.parseInt("foo"); // NaN
Array.indexOf([0, 1, 2], 3); // -1Array.firstOr([], 0); // 0
String.parseIntOr("foo", 0); // 0
Array.indexOfOr([0, 1, 2], 3, 0); // 0Array.firstOrElse([], (arr) => arr.length); // 0
String.parseIntOrElse("foo", (str) => str.length); // 3
Array.indexOfOrElse([0, 1, 2], 3, (arr) => arr.length); // 3Array.firstOrThrow([]); // Throws FnError
String.parseIntOrThrow("foo"); // Throws FnError
Array.indexOfOrThrow([0, 1, 2], 3); // Throws FnErrorNamespaces
This library primarily exports namespaces, for example:
import { Object } from "@monstermann/fn/object";
Object.merge(a, b);In-editor discovery
With namespaces, you can easily discover what is available, without having to constantly pull up the documentation and memorize all the function names:
import { Array } from "@monstermann/fn";
Array.|
// ^ Your editor will show everything that is available!Consistent naming
Usually utility libraries not designed around with namespaces struggle with naming things.
The function drop(target, amount) could support both arrays and strings for example, however you are forced to come up with a unique and semantically meaningful name for each possibility.
So the bigger a utility becomes, the more difficult it is to maintain it. Using namespaces allows us to keep things simple:
import { drop, sliceFrom } from "…";
drop([1, 2, 3], 2); // [3]
sliceFrom("Hello World!", 6); // "World!"import { Array, String } from "@monstermann/fn";
Array.drop([1, 2, 3], 2); // [3]
String.drop("Hello World!", 6); // "World!"Type hints
In long and complex pipelines, it can sometimes be difficult to maintain a mental model of what data is flowing through, especially when you are not yet familiar with every single function.
While you could hover all parts in your editor to read the type signatures, namespaces based on types can give you a decent glance of what is happening:
pipe(
["foo", "bar", "baz"],
mapEach(parseInt()),
mapEach(toString()),
mapEach(dropLast(1)),
join(""),
pascalCase(),
append(" Batman!"),
); //=> Nanana Batman!pipe(
["foo", "bar", "baz"],
Array.mapEach(String.parseInt()),
Array.mapEach(Number.toString()),
Array.mapEach(String.dropLast(1)),
Array.join(""),
String.pascalCase(),
String.append(" Batman!"),
); //=> Nanana Batman!Native aliases
This library comes with additional aliases for most natively available methods, sometimes with more convenient type definitions.
This can prevent having to memorize what is natively available and what is available as an external utility.
"Hello World!".indexOf("W"); // 6
String.indexOf("Hello World!", "W"); // 6pipe("Hello World!", (str) => str.indexOf("W")); // 6
pipe("Hello World!", String.indexOf("W")); // 6Installation
npm install @monstermann/fnpnpm add @monstermann/fnyarn add @monstermann/fnbun add @monstermann/fnTree-shaking
Installation
npm install -D @monstermann/unplugin-fnpnpm -D add @monstermann/unplugin-fnyarn -D add @monstermann/unplugin-fnbun -D add @monstermann/unplugin-fnUsage
// vite.config.ts
import fn from "@monstermann/unplugin-fn/vite";
export default defineConfig({
plugins: [fn()],
});// rollup.config.js
import fn from "@monstermann/unplugin-fn/rollup";
export default {
plugins: [fn()],
};// rolldown.config.js
import fn from "@monstermann/unplugin-fn/rolldown";
export default {
plugins: [fn()],
};// webpack.config.js
const fn = require("@monstermann/unplugin-fn/webpack");
module.exports = {
plugins: [fn()],
};// rspack.config.js
const fn = require("@monstermann/unplugin-fn/rspack");
module.exports = {
plugins: [fn()],
};// esbuild.config.js
import { build } from "esbuild";
import fn from "@monstermann/unplugin-fn/esbuild";
build({
plugins: [fn()],
});