mrkeyoor.com_
Fri 07 Aug 19:01 UTC
npmUtilsupdated 07 Aug 2026

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.

Verdict

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.

API stability4/5v2 landed in 2024 and has stayed on 2.x through 39 minor releases, with new functions added rather than old ones changed. Deprecations are handled in the type definitions with a pointer to the replacement, as with debounce to funnel, but that pattern also means the surface accumulates functions you should not use and the compiler is the only thing telling you.
Docs5/5remedajs.com/docs has every function with signature, both call forms, runnable examples and category grouping, all generated from JSDoc so the in-editor tooltips match the site exactly. The lodash and ramda migration guides map function to function, which is more transitional help than almost any library in this category provides.
Maintenance4/5Steady and organized: five releases between 2026-05-25 and 2026-06-09, the repo pushed 2026-08-05, and only 16 open issues (26 counting PRs) against 5410 stars. The recent commit stream is mostly dependabot bumps on the docs site rather than library work, so feature velocity is slower than the push date suggests.
Ecosystem3/5About 9.2M weekly downloads and a real following in TypeScript-heavy codebases, but it is the third choice in a crowded slot: lodash still owns the mindshare and es-toolkit is taking the new projects. Very little else depends on it directly, so adoption is a per-team decision rather than something your dependency tree drags in.

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
Skip it if

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] 2

map 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 sort

firstBy 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

PackageRegistryPick it when
es-toolkitnpmYou want the fastest-moving modern option and a lodash-compatible entry point for a mechanical migration
lodash-esnpmYou need the full lodash surface, every developer already knows it, and you can live with loose types
ramdanpmYou want strict functional style with automatic currying everywhere and are willing to accept weaker TypeScript inference
radashnpmYou want a small dependency-free helper set with plain signatures and no pipe or currying to learn