remeda review
Remeda is a JavaScript collection and object utility library built around TypeScript inference and two call shapes. A function can receive data first in an ordinary call or data last inside pipe; supported pipe steps evaluate lazily. The package provides ESM and CommonJS entry conditions with no runtime dependencies. Current version 2.44.0 fixes filter and groupByProp type predicates whose narrowed type is not a subtype of the input item. Our measured install used 2.42.0, where both require and ESM import worked and a whole-package browser import measured 9 KB gzipped.
Remeda earns its place in TypeScript code that benefits from narrowing and lazy data-last pipelines. Skip it for plain JavaScript or a project already served by native methods, and verify the declaration artifact because our measured 2.42.0 package check found none.
We installed it
| Install | ✓ · 0.5s | 1 package on disk · 5 MB |
| Import | ✓ | ESM import works · require() works · ESM package with exports map |
| Browser | 9 KB | gzipped (27.8 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does remeda install cleanly?
Yes. In a fresh container with an empty cache, npm install remeda finished in 0.5s, leaving 1 package and 5 MB on disk. npm audit reported no known vulnerabilities.
How much does remeda add to a browser bundle?
9 KB gzipped (27.8 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does remeda work with both ESM and CommonJS?
Yes. Both import 'remeda' and require('remeda') worked in Node 22 in our run. The package is published as ESM with an exports map.
Does remeda include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
remeda or lodash-es: which should you use?
lodash-es: Use it when Lodash API coverage and team familiarity matter more than the sharpest TypeScript inference. Remeda earns its place in TypeScript code that benefits from narrowing and lazy data-last pipelines.
When should you not use remeda?
The project is plain JavaScript. Most of Remeda's advantage is encoded in TypeScript signatures and editor feedback
Use it if
- TypeScript inference must preserve narrowed unions, object keys, tuples, and predicate results through utility calls
- Pipelines should accept data-last operations while ordinary code keeps a data-first form
- Filtering, mapping, and taking a few results should stop early instead of materializing every intermediate array
- A Lodash or Ramda migration benefits from function-by-function documentation
- The project is plain JavaScript. Most of Remeda's advantage is encoded in TypeScript signatures and editor feedback
- Native array and object methods already cover the handful of operations in use. Another vocabulary can cost more than four small helpers save
- The code depends on Lodash path strings, chaining, templates, or class-aware deep cloning. Remeda does not mirror that complete API
- The team wants one call convention. Data-first and data-last overloads make pipe code concise but can produce hard-to-read type errors when mixed up
- Compiler performance is already strained by conditional and recursive types. Remeda's precise inference should be measured on the real project
Setup reality
We installed Remeda 2.42.0 in a clean Node 22 Bookworm container. npm completed in 0.5 seconds and left one package using 5 MB. The package has zero direct and zero peer dependencies, with 4,736 KB unpacked, and requires Node 18 or newer. npm audit found no known vulnerabilities at any severity. Our package inspection found no TypeScript declaration files in that measured artifact.
Version 2.42.0 is an ESM package with an exports map, yet both require() and ESM import worked because the root export supplies separate conditions. Do not assume internal subpaths or package.json are importable through that map. Current registry version 2.44.0 is newer than the measured sandbox and fixes type-predicate support in filter and groupByProp, so repeat your own artifact and declaration checks when upgrading.
A whole-package esbuild import measured 27.8 KB minified and 9 KB gzipped. Named imports and production tree shaking can reduce application output, but that depends on the bundler and import style; the measured number is the safe reference for a namespace import. There is no plugin or runtime configuration. Most setup time goes into agreeing on data-first calls outside pipe and data-last calls inside it.
Lazy evaluation applies only to compatible pipeline operations. A step such as sortBy or groupBy must consume the whole input and becomes a materialization boundary. Zero-argument data-last helpers still need parentheses, such as unique(), because the call constructs the operation. Prefer funnel over the deprecated debounce helper documented with known implementation issues.
Patterns
Stop a lazy pipeline after enough matches lazy-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] 2Lazy-capable steps pull values one at a time. Sorting or grouping must first consume the complete collection.
Switch between the two call shapes data-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());A zero-argument operation still needs a call, such as unique(), to create its data-last form.
Remove nullish values and narrow the type narrow-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()),
);isNonNullish rejects null and undefined; isDefined keeps null while removing undefined.
Group records or index them by a field group-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 can omit an item when the key callback returns undefined. groupByProp preserves property-driven key types.
Sort by keys or find one extreme sort-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 avoids sorting the entire array when only one winning item is needed.
Reshape object keys reshape-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 }These helpers copy shallowly, so nested object references remain shared with the source.
Aggregate numeric properties aggregate-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 returns zero for an empty input, while meanBy returns NaN. Handle empty chart series explicitly.
Partition or chunk a collection split-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 typed pair. chunk rejects sizes below one and leaves the final group short.
Merge or transform selected fields merge-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 replaces arrays instead of concatenating them and only recurses through plain objects.
Control bursts with funnel debounce-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" });Use funnel for debounce or throttle timing; the older debounce helper is deprecated because of known issues.
Run a conditional pipe step conditional-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 original value through on a failed predicate. Add a fallback case when conditional is not exhaustive.
Build a reusable operation pipeline reusable-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);Annotate the first input when inference has no concrete data value from which to start.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| lodash-es | npm | Use it when Lodash API coverage and team familiarity matter more than the sharpest TypeScript inference. |
| rambda | npm | Use it for a smaller Ramda-style functional API with automatic currying expectations. |
| radash | npm | Use it for dependency-free helpers with straightforward signatures and less emphasis on lazy pipes. |
More utils guides
lru-cache · ajv · type-fest · p-limit · find-up · js-yaml · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

