remeda
remeda is a utility library in the lodash and ramda family, written for TypeScript from the start. It exports about 163 functions covering arrays, objects, strings, numbers, guards and function control, and every one of them works two ways: data-first, filter(users, isActive), for a plain call, and data-last, filter(isActive), for use inside pipe(). The point of difference is the type layer. The signatures are written to keep as much information as the compiler had going in, so picking two keys off a literal object gives you an object with those two keys rather than Partial<T>, splitting a tuple gives you tuples, and a guard passed to filter narrows the element type of the result. pipe() is also lazy for the functions that can be: filter then take(2) stops iterating once it has two matches instead of walking the whole array. There are no runtime dependencies, the package is ESM-first with a CommonJS build alongside, and it is marked side-effect free so bundlers drop what you do not import.
The right pick when your codebase is TypeScript-first and you are tired of utility functions that throw away type information, and the lazy pipe is a genuine advantage over both lodash and native methods. If you are on plain JavaScript, or you only need four helpers that the language already has, this is a dependency you can skip.
Use it if
- You are on TypeScript and lodash's types keep widening things you care about: remeda preserves literal types, tuple lengths and key sets through pick, omit, entries, splitAt and friends instead of collapsing them
- You want lazy pipelines: pipe(items, filter(pred), map(fn), take(3)) evaluates element by element and stops early, so map runs three times rather than once per item
- You want a type guard to actually narrow: filter(isNonNullish) turns (T | null | undefined)[] into T[] at the type level, which is the single most common reason people reach for this over the built-in methods
- You are migrating off lodash or ramda and want a mapping table rather than a rewrite: remedajs.com publishes per-function migration guides for both
- You need debounce and throttle behaviour you can reason about: funnel() replaces both with one primitive that has explicit triggerAt, minQuietPeriodMs, minGapMs and maxBurstDurationMs options plus cancel, flush and isIdle
- You are writing plain JavaScript. Nearly all the value here is in the type signatures, and without a compiler reading them you are choosing a smaller, less-known library over lodash for no gain
- Modern JavaScript already covers your list. Array.prototype.flatMap, Object.groupBy, Object.entries, Array.prototype.at, toSorted and structuredClone handle most of what people historically imported a utility library for, and adding 163 functions to reach for four of them is weight you do not need
- Slow type checking would hurt. The types are deliberately as specific as the compiler allows, which means conditional and recursive types on the hot path of tsc. On large codebases that shows up in check time, and when a call does not line up the error points at the pipe() as a whole rather than at the step you got wrong
- You want lodash's full surface. There is no _.get with a dotted path string, no chain, no template, and no deep clone of class instances. debounce exists but is marked deprecated in the type definitions with a note that the implementation has known issues and funnel is the replacement
- The two call conventions will bite your team. filter(data, fn) and filter(fn) are the same function, unique() needs the empty parentheses inside a pipe, and getting it backwards produces a type error about an unexpected function rather than anything that names the real mistake
- Your tooling probes package files. The exports map declares only ".", so require('remeda/package.json') throws ERR_PACKAGE_PATH_NOT_EXPORTED, and anything that resolves subpaths for version detection needs a workaround
- You want the most active option in this category. remeda is healthy, but es-toolkit ships faster, publishes a lodash-compatible entry point for drop-in migration, and is where a lot of the ecosystem's attention moved
Setup reality
npm install remeda and there is nothing else to configure: zero runtime dependencies, no plugin, no babel transform, no peer requirements. The package is type: module with a CommonJS build behind the exports condition, so both require and import resolve, and Node 18 or newer is declared in engines. The full barrel import measures 9.2 kB gzipped, but sideEffects is false and every function lives in its own module, so a real app that imports eight functions ships a fraction of that as long as your bundler is doing tree-shaking (Vite, Rollup, esbuild and webpack in production mode all do; a plain ts-node script does not). The learning curve is the actual setup cost. Every function has two shapes and you have to know which one you are in: outside pipe you pass the data first, inside pipe you omit it, and functions with no other arguments still need calling, so it is unique() and not unique. Older lodash habits fail loudly: there is no chaining, no _.get('a.b.c'), and prop('name') is how you point at a field. Read the migration guide for whichever library you are leaving before you write the first pipe, because the naming is close enough to be misleading.
Patterns
Build a pipeline that stops earlylazy-pipe
import { pipe, map, filter, take } from "remeda";
let calls = 0;
const result = pipe(
[1, 2, 3, 4, 5, 6],
map((x) => { calls++; return x * 2; }),
take(2),
);
console.log(result, calls); // [2, 4] 2map ran twice, not six times, because pipe pulls elements through lazily for the functions that support it. Not every function is lazy: sortBy and groupBy have to see the whole collection, and they act as a barrier that materializes everything upstream of them.
Know which call shape you are indata-first-vs-data-last
import { pipe, filter, unique } from "remeda";
// data-first: standalone call
const evens = filter([1, 2, 3, 4], (x) => x % 2 === 0);
// data-last: inside a pipe, data argument omitted
const deduped = pipe([1, 2, 2, 3], filter((x) => x > 1), unique());unique() needs the parentheses even with no arguments, because the call is what produces the data-last operation. Writing unique without them passes the function itself and the type error talks about an incompatible signature rather than the missing call.
Filter out null and undefined and have the type follownarrow-nullish
import { pipe, filter, map, isNonNullish } from "remeda";
const ids: (string | null | undefined)[] = raw;
const clean: string[] = pipe(
ids,
filter(isNonNullish),
map((id) => id.trim()),
);Array.prototype.filter with the same predicate leaves the type as (string | null | undefined)[] unless you write a manual type predicate. isNonNullish rejects both null and undefined; isDefined only rejects undefined, which is the difference people get wrong.
Group rows or key them by idgroup-and-index
import { groupBy, groupByProp, indexBy, prop } from "remeda";
const byStatus = groupBy(orders, prop("status"));
// { paid: Order[], pending: Order[] }
const byStatusTyped = groupByProp(orders, "status"); // better key typing
const usersById = indexBy(users, prop("id"));groupBy lets the callback return undefined to drop an item entirely, which Object.groupBy cannot do. When you are grouping on a plain property, groupByProp gives sharper key types than passing prop(), and the docs say so.
Sort by several keys, or just take the extremesort-and-pick-top
import { sortBy, firstBy, prop } from "remeda";
sortBy(rows, prop("team"), prop("score")); // ascending, then ascending
sortBy(rows, [prop("score"), "desc"]); // descending
const best = firstBy(rows, [prop("score"), "desc"]); // no full sortfirstBy is O(n) and returns undefined on an empty input, so it beats sortBy(...)[0] whenever you only need the winner. sortBy returns a new array and leaves the input alone, unlike Array.prototype.sort.
Pick, omit and map values without losing key typesreshape-objects
import { pick, omit, mapValues } from "remeda";
const summary = pick(user, ["id", "email"]); // { id: number; email: string }
const safe = omit(user, ["passwordHash"]); // every key except that one
const doubled = mapValues({ a: 1, b: 2 }, (v) => v * 2); // { a: number; b: number }The returned type has exactly the keys you asked for, not Partial<T>, which is where lodash's typings give up. All three copy shallowly, so nested objects are shared with the original and mutating them mutates both.
Sum, average and count without a reduceaggregate-numbers
import { sumBy, meanBy, countBy, prop } from "remeda";
sumBy(lineItems, prop("cents"));
meanBy(scores, prop("value"));
countBy(events, (e) => (e.ok ? "ok" : "failed")); // { ok: 12, failed: 3 }sumBy on an empty array returns 0 while meanBy returns NaN, so a chart fed by an empty filter renders nothing rather than a zero. countBy keys are typed from what your callback can return, so returning a template string widens them to string.
Partition or chunk a listsplit-collections
import { partition, chunk } from "remeda";
const [passed, failed] = partition(results, (r) => r.ok);
for (const batch of chunk(ids, 100)) {
await api.deleteMany(batch);
}partition returns a two-element tuple typed as such, so destructuring keeps both sides typed. chunk throws on a size below 1 instead of looping forever, and the final chunk is short rather than padded.
Merge deeply or transform selected fieldsmerge-and-evolve
import { mergeDeep, evolve } from "remeda";
const config = mergeDeep(defaults, overrides);
// { a: { x: 1, y: 2 } } from { a: { x: 1 } } and { a: { y: 2 } }
const normalized = evolve(row, {
email: (e) => e.toLowerCase(),
createdAt: (d) => new Date(d),
});mergeDeep recurses into plain objects only: arrays are replaced wholesale, not concatenated, which is usually what you want for config and never what you want for lists. evolve leaves any key you did not name untouched, so it is safe on wide rows.
Debounce or throttle with one primitivedebounce-with-funnel
import { funnel } from "remeda";
const search = funnel(() => runSearch(inputRef.current.value), {
minQuietPeriodMs: 250,
});
search.call();
const scroll = funnel(() => measure(), { minGapMs: 100, triggerAt: "start" });minQuietPeriodMs gives you debounce, minGapMs with triggerAt start gives you throttle, and maxBurstDurationMs caps a burst that keeps extending itself. Use funnel rather than debounce: the older function is deprecated in the type definitions with a note that its implementation has known issues.
Apply a step only when a condition holdsconditional-transform
import { pipe, when, conditional, constant } from "remeda";
pipe(200, when((p) => p > 100, (p) => p * 0.9)); // 180
pipe(50, when((p) => p > 100, (p) => p * 0.9)); // 50
const label = conditional(
status,
[(s) => s === "paid", () => "Paid"],
[(s) => s === "open", () => "Awaiting payment"],
constant("Unknown"),
);when passes the value through unchanged when the predicate fails, which keeps a pipe readable without an if. conditional throws Error('conditional: data failed for all cases') when nothing matches, so end the list with a bare fallback function such as constant(...) unless the input is an exhaustive union.
Name a pipeline and reuse itreusable-pipeline
import { piped, map, filter, isNonNullish } from "remeda";
const activeNames = piped(
filter((u: User) => u.active),
map((u) => u.displayName),
filter(isNonNullish),
);
const names = activeNames(users);piped composes data-last steps into one function without needing data yet, which is what you want for a callback passed to something else. Annotate the parameter of the first step, because there is no input value for the compiler to infer from.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| es-toolkit | npm | You want the fastest-moving modern option and a lodash-compatible entry point for a mechanical migration |
| lodash-es | npm | You need the full lodash surface, every developer already knows it, and you can live with loose types |
| ramda | npm | You want strict functional style with automatic currying everywhere and are willing to accept weaker TypeScript inference |
| radash | npm | You want a small dependency-free helper set with plain signatures and no pipe or currying to learn |