npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

modern-fns

v1.0.1

Published

Modern JavaScript & TypeScript utility library — 145 modular, immutable, tree-shakeable, dependency-free utilities. A lightweight alternative to Lodash.

Readme

modern-fns

Modern utility functions for JavaScript and TypeScript — modular, immutable, tree-shakeable and dependency-free.

Buy Me A Coffee

145 focused utilities for the problems frontend and backend developers solve by hand over and over: reading a nested path safely, diffing two objects for a PATCH body, editing a query string without losing the hash, parsing "₹1,299.50" into a number, formatting 1500000 as "1.5M".

npm install modern-fns
import { groupBy, set, diff, slugify, currency } from 'modern-fns';

groupBy(orders, 'status'); // { paid: [...], pending: [...] }
set(state, 'user.address.city', 'Kochi'); // immutable — `state` untouched
diff(pristine, form); // [{ path: 'name', type: 'changed', ... }]
slugify('Hello Vue World!'); // 'hello-vue-world'
currency(1299.5, 'INR', 'en-IN'); // '₹1,299.50'

| | | | ----------------------------- | ----------------------------------------------------------------------------------------- | | Zero runtime dependencies | Nothing but the platform. No Lodash, no qs, no deepmerge. | | Tree-shakeable | One function per file. import { chunk } ships 0.20 kB gzipped. | | Immutable | No exported function mutates its input. Ever. | | TypeScript-first | Written in strict TS, generics that infer, no any in the public API. | | Universal | Vue, React, Nuxt, Next, Svelte, Node, browsers, workers — no Node built-ins. | | ESM + CJS | Modern exports map with import/require/types conditions. | | Tested | 363 tests, 99.4% line coverage; ESM + CJS consumers typechecked against the real tarball. |


Why modern-fns?

Most JavaScript utility libraries were designed before ES modules, before TypeScript was the default, and before bundle size was a shipping constraint. modern-fns is built for the way applications are written now.

  • Zero runtime dependencies — nothing but the platform, enforced by a CI check.
  • Immutable operations — no exported function mutates its input, so === change detection in Vue, React and Svelte stays correct.
  • TypeScript-first — written in strict TypeScript, types generated from the source, no @types/* package to install and no any in the public API.
  • Tree-shakeable — one function per file and sideEffects: false, so import { chunk } ships 0.20 kB gzipped rather than a library.
  • ESM and CommonJS — a modern exports map with per-condition types, verified by typechecking real ESM and CJS consumer projects in CI.
  • Per-function imports — modern-fns/array/chunk resolves to exactly one module.
  • Runs anywhere — Node 18+, all modern browsers, and any runtime with ES2021 and Intl. The package imports no Node built-ins, which CI enforces.

modern-fns vs Lodash

Lodash is the reference point for this category, so here is an honest comparison. Facts about Lodash below were checked against [email protected] on the npm registry.

| | modern-fns | Lodash | | ---------------------------- | ------------------------------------ | ------------------------------------------------ | | Runtime dependencies | 0 | 0 | | Bundled TypeScript types | Yes, generated from source | No — install @types/lodash separately | | ESM | Native | Separate lodash-es package | | exports map | Yes, with import/require/types | No exports field | | Tree-shaking from main entry | Yes (sideEffects: false) | Needs lodash-es or per-method imports | | Per-function imports | modern-fns/array/chunk | lodash/chunk | | Immutability | Every function | Mixed — set, pull, remove, assign mutate | | Selector ergonomics | keyof T or a function, everywhere | Strings, paths, objects, matchers ("iteratee") | | API surface | 145 functions | ~300 functions |

When Lodash is still the better choice: you need its breadth (_.template, _.curry, the full fp module), you are on a legacy CommonJS toolchain, or you want a library with a decade of production hardening behind it. modern-fns deliberately covers less ground — see Design philosophy for what it declines to reimplement.


Table of contents


Quick start

import { changed, chunk, diffArray, mergeQuery, orderBy, pipe, toNumber } from 'modern-fns';

// Reshape API data
chunk(products, 24); // pages
orderBy(users, ['role', 'age'], ['asc', 'desc']); // multi-key sort, immutable

// Know exactly what changed
changed(pristine, form); // is the form dirty?
diffArray(oldRows, newRows, 'id'); // { added, removed, updated, unchanged }

// URLs without string surgery
mergeQuery('/products?page=2&sort=price', { page: 3, q: 'shoes' });
// '/products?page=3&sort=price&q=shoes'

// Trust nothing from the wire
toNumber('₹1,299.50'); // 1299.5
toNumber('abc', 0); // 0

// Compose
const toHandle = pipe((s: string) => s.trim(), slugify);

Import styles

All three are tree-shakeable. Named imports are the recommended default.

// 1. Named — recommended
import { chunk, groupBy, diff } from 'modern-fns';

// 2. Subpath — maximally explicit, provably one module
import chunk from 'modern-fns/array/chunk';
import diff from 'modern-fns/diff/diff';

// 3. Namespaces — useful for the query module
import { array, object, string, number, url, query, value } from 'modern-fns';
query.parse('?page=2&tags=vue&tags=nuxt'); // { page: 2, tags: ['vue', 'nuxt'] }

CommonJS works too:

const { chunk } = require('modern-fns');

One naming rule to know: the query module's functions (parse, get, set, has, merge, remove, toggle, …) are not flat-exported from the package root, because they would collide with the object module's get/set/has. Use query.parse(...) or modern-fns/query/parse. Everything else is available as a named import.

Modules

| Module | What it covers | Docs | | ------------ | ----------------------------------------------------------------------------- | ------------------------------------------ | | array | 25 list utilities plus diffArray for list reconciliation | docs/array.md | | object | Path get/set/unset, pick/omit, deep clone/merge, structural equality, flatten | docs/object.md | | diff | Structural change sets: dirty checking, audit logs, PATCH, undo/redo | docs/diff.md | | string | Case conversion, slugs, truncation, masking, sanitising, extraction | docs/string.md | | number | Decimal-safe maths, percentages, business maths, Intl formatting | docs/number.md | | url | Absolute and relative URL manipulation, query and hash editing | docs/url.md | | query | Query-string parse/stringify with configurable rules and typed readers | docs/query.md | | value | Safe coercion and type guards for untrusted input | docs/value.md | | functional | pipe, memoize, debounce, throttle, tryCatch, … | docs/functional.md | | collection | One iteration API across arrays, objects, Map, Set, iterables | docs/collection.md |

The full signature list lives in the API specification.

Highlights

Immutable deep updates that keep references stable

import { set, unset, get } from 'modern-fns';

const next = set(state, 'user.profile.city', 'Kochi');
next !== state; // true — the path was copied
next.user.roles === state.user.roles; // true — untouched branches keep their reference
get(data, 'users[0].address.city', 'n/a'); // paths support brackets and defaults

That reference stability is what makes === change detection in Vue, React and Svelte cheap and correct.

A diff you can actually use

import { diff, changed, patch } from 'modern-fns';

changed(pristine, form); // enable/disable Save

diff(pristine, form);
// [
//   { path: 'name',         type: 'changed', oldValue: 'John', newValue: 'Vishnu' },
//   { path: 'address.city', type: 'added',   newValue: 'Kochi' },
// ]

patch(pristine, diff(pristine, form)); // structurally equal to `form`

Configurable array strategies ('index', 'whole', 'key'), custom equality per path, and an ignore hook for server-managed fields like updatedAt.

List reconciliation

import { diffArray } from 'modern-fns';

diffArray(oldUsers, newUsers, 'id');
// added:     items only in the new list
// removed:   items only in the old list
// updated:   [{ key, before, after, changes: [...] }]  — with field-level changes
// unchanged: deep-equal items

Reordering is not a change. An edit is one updated entry, not a delete plus an insert.

URLs and query strings that survive real input

import { setQuery, mergeQuery, isSameUrl } from 'modern-fns';
import { query } from 'modern-fns';

setQuery('/products?page=2', 'sort', 'price'); // '/products?page=2&sort=price'
mergeQuery('/p?page=2&sort=price', { sort: undefined }); // '/p?page=2'
isSameUrl('/p?a=1&b=2', '/p/?b=2&a=1'); // true
query.parse('?page=2&tags=vue&tags=nuxt'); // { page: 2, tags: ['vue', 'nuxt'] }
query.toggle('?page=2', 'inStock'); // '?page=2&inStock=true'

Relative in, relative out. Arrays, nested keys, hashes and encoding all handled.

Coercion without the JavaScript traps

import { toNumber, toBoolean, isEmpty, isNumeric } from 'modern-fns';

toNumber('₹1,299.50'); // 1299.5      (currency and separators)
toBoolean('false'); // false       (Boolean('false') is true)
isNumeric(''); // false       (Number('') is 0)
isEmpty(0); // false       (0 is a value, not an absence)

Every conversion is documented as an input/output table — see docs/value.md.

Numbers that behave

import { round, abbreviate, currency, percentage } from 'modern-fns';

round(1.005, 2); // 1.01   (Math.round gives 1)
abbreviate(1500000); // '1.5M'
currency(1299.5, 'INR', 'en-IN'); // '₹1,299.50'
percentage(5, 0); // 0      (not NaN)

TypeScript

Written in TypeScript with strict: true, shipped with complete .d.ts files and source maps. No any in the public API.

Generics infer through the call.

const byRole = groupBy(users, 'role'); // Record<string, User[]>
const [active, rest] = partition(users, (u) => u.isActive); // [User[], User[]]
const names = compact(list); // (string | null)[] -> string[]

Property selectors are checked.

groupBy(users, 'role'); // ok
groupBy(users, 'rolle'); // compile error: not a keyof User
groupBy(users, (u) => u.role.toUpperCase()); // functions welcome too

Type guards narrow.

if (isString(input)) input.trim(); // input: string
const numbers = mixed.filter(isNumber); // number[]

Discriminated unions where they help.

for (const change of diff(before, after)) {
  if (change.type === 'added') change.newValue; // no `oldValue` in scope
}

Overloads for real signatures.

toNumber('42'); // number
toNumber(input, 0); // number
toDate(value); // Date | undefined
toDate(value, new Date()); // Date
pipe(fetchUser, (r: Response) => r.json()); // returns a Promise — inferred
pipe((n: number) => n + 1); // stays synchronous — inferred

Immutability

Every transformation returns a new value. Nothing sorts in place, splices your array or writes to your object.

const original = { user: { name: 'John' } };
const updated = set(original, 'user.name', 'Vishnu');
original.user.name; // 'John'

Three functions own internal state, and say so in their docs: debounce, throttle and memoize. tap invokes a callback you provide — if that callback mutates, tap cannot stop it. Nothing else in the library holds state.

set and unset also refuse paths containing __proto__, constructor or prototype, so a path from user input cannot pollute Object.prototype.

Bundle size and tree shaking

The published dist/ mirrors src/ file for file — the package is compiled with tsc, not bundled. Your bundler sees one function per module and can drop everything else without heuristics. sideEffects: false is declared, and no module runs code at import time.

Measured with esbuild (minified + gzipped), enforced in CI by npm run size:

| Import | Gzipped | | --------------------------------- | -------- | | modern-fns/array/chunk | 0.20 kB | | modern-fns/object/set | 0.53 kB | | modern-fns/value/toNumber | 0.40 kB | | modern-fns/functional/debounce | 0.33 kB | | modern-fns/diff/diff | 1.35 kB | | modern-fns/array (whole module) | 2.91 kB | | modern-fns (everything) | 12.83 kB |

// Ships one function, not a library.
import { chunk } from 'modern-fns';

Verified with Vite, Rollup, webpack 5 and esbuild. If your bundler is configured for CJS only, prefer subpath imports — CJS cannot be tree-shaken by anyone.

Browser and runtime support

| Environment | Minimum | | ------------------------------------------ | ------- | | Node | 18 | | Chrome / Edge | 90 | | Firefox | 90 | | Safari | 15 | | Deno, Bun, Cloudflare Workers, Vercel Edge | current |

Node and browser figures are the supported floor; the ESM build is exercised on Node 16 and 24 and in Chrome in CI. Deno, Bun and edge runtimes are expected to work because the package uses no Node built-ins, but they are not part of the test matrix.

The library targets ES2021 and uses only Intl.NumberFormat, URL, String.prototype.normalize and standard collections. There are no Node built-ins, no DOM requirements and no polyfills — the same file runs in a browser, a server and a worker.

Design philosophy

Every function had to answer one question before it was added:

What recurring developer pain does this eliminate?

That is why this is not a Lodash clone. There is no map, filter, head, sum, isNaN or forEach shim — the native versions are good. What is here is the code you would otherwise write by hand and get subtly wrong:

  • round(1.005, 2) is 1.01. The naive version gives 1.
  • sortBy returns a new array. Array.prototype.sort mutates.
  • toBoolean('false') is false. Boolean('false') is true.
  • isNumeric('') is false. Number('') is 0.
  • zip pads to the longest input instead of silently dropping data.
  • random(1, 6) includes 6.
  • abbreviate(1999) is '1.9K' — a count never reads higher than it is.
  • pick skips absent keys instead of adding undefined to your PATCH body.
  • unset on an array index splices instead of leaving a hole.
  • debounce returns cancel(), so unmounting does not set state on a dead component.

And where a name is risky, there is an escape hatch: slidingWindow aliases window so a named import cannot shadow the DOM global, and the query module is namespaced away from object.

Priorities, in order: practicality, predictable APIs, type safety, small bundles, tree shaking, immutability, documentation, edge-case handling, composability.

API documentation

Full documentation with a page per module is published at modern-fns.vercel.app — see the Lodash migration guide and the comparison.

Per-function reference — name, description, signature, parameters, return value, examples, edge cases and TypeScript notes:

array · object · diff · string · number · url · query · value · functional · collection

Design notes: the API specification (signatures, naming conflicts, bundle strategy) and the API review (what was cut, what is on probation, and why).

Runnable examples: a Node tour and a Vue 3 playground that exercises every exported function with live, editable inputs.

Every function also carries full JSDoc, so your editor shows the same information on hover.

Contributing

See CONTRIBUTING.md — it covers the local workflow and the bar a new utility has to clear.

npm install
npm run dev            # vitest, watch mode
npm run ci             # lint + typecheck + coverage + build + verify + size

Support

modern-fns is free and dependency-free, and stays that way. If it saved you an afternoon, you can buy me a coffee.

License

MIT © Vishnu M