Skip to content

fn

A utility library for TypeScript.

This package composes several libraries into one:

Features

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!

copymutation
ts
const a = [];
const b = Array.append(a, 0);
const c = Array.append(b, 1);
const d = Array.append(c, 2);
ts
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.

ts
const before = { foo: true, bar: false };
const after = Object.merge(before, { foo: true });
before === after; // true
ts
const before = [1, 2, 3];
const after = Array.filter(before, (value) => value > 0);
before === after; // true
Example implementation
js
const before = [1, 2, 3];
const after = mapEach(before, (value) => value);
before === after; // true
js
// Short and simple, but inefficient:
function mapEach(before, mapper) {
    const after = before.map(mapper);
    return isShallowEqual(before, after) ? before : after;
}
js
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.

ts
Array.findRemove([1, 2, 3, 4], (x) => x > 2); // [1, 2, 4]
ts
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:

ts
Array.first([]); // undefined
String.parseInt("foo"); // NaN
Array.indexOf([0, 1, 2], 3); // -1
ts
Array.firstOr([], 0); // 0
String.parseIntOr("foo", 0); // 0
Array.indexOfOr([0, 1, 2], 3, 0); // 0
ts
Array.firstOrElse([], (arr) => arr.length); // 0
String.parseIntOrElse("foo", (str) => str.length); // 3
Array.indexOfOrElse([0, 1, 2], 3, (arr) => arr.length); // 3
ts
Array.firstOrThrow([]); // Throws FnError
String.parseIntOrThrow("foo"); // Throws FnError
Array.indexOfOrThrow([0, 1, 2], 3); // Throws FnError

Namespaces

This library primarily exports namespaces, for example:

ts
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:

ts
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:

ts
import { drop, sliceFrom } from "…";

drop([1, 2, 3], 2); // [3]
sliceFrom("Hello World!", 6); // "World!"
ts
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:

ts
pipe(
    ["foo", "bar", "baz"],
    mapEach(parseInt()),
    mapEach(toString()),
    mapEach(dropLast(1)),
    join(""),
    pascalCase(),
    append(" Batman!"),
); //=> Nanana Batman!
ts
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.

ts
"Hello World!".indexOf("W"); // 6
String.indexOf("Hello World!", "W"); // 6
ts
pipe("Hello World!", (str) => str.indexOf("W")); // 6
pipe("Hello World!", String.indexOf("W")); // 6

Installation

sh
npm install @monstermann/fn
sh
pnpm add @monstermann/fn
sh
yarn add @monstermann/fn
sh
bun add @monstermann/fn

Tree-shaking

Installation

sh
npm install -D @monstermann/unplugin-fn
sh
pnpm -D add @monstermann/unplugin-fn
sh
yarn -D add @monstermann/unplugin-fn
sh
bun -D add @monstermann/unplugin-fn

Usage

ts
// vite.config.ts
import fn from "@monstermann/unplugin-fn/vite";

export default defineConfig({
    plugins: [fn()],
});
ts
// rollup.config.js
import fn from "@monstermann/unplugin-fn/rollup";

export default {
    plugins: [fn()],
};
ts
// rolldown.config.js
import fn from "@monstermann/unplugin-fn/rolldown";

export default {
    plugins: [fn()],
};
ts
// webpack.config.js
const fn = require("@monstermann/unplugin-fn/webpack");

module.exports = {
    plugins: [fn()],
};
ts
// rspack.config.js
const fn = require("@monstermann/unplugin-fn/rspack");

module.exports = {
    plugins: [fn()],
};
ts
// esbuild.config.js
import { build } from "esbuild";
import fn from "@monstermann/unplugin-fn/esbuild";

build({
    plugins: [fn()],
});

Credits