Repository navigation
[Replacement]: lodash #1123
Description
Package to replace
lodash (together with lodash-es and underscore, which share the lodash-underscore page)
Suggested replacement(s)
remeda
Manifest type
preferred (lighter or more modern alternative package)
Rationale
Disclosure first: I maintain Remeda.
Remeda is a TypeScript-first utility library, and the main reason to migrate to it from Lodash is that it moves errors from runtime to compile time. Lodash's types are permissive by design, so a whole class of mistakes compiles fine and only shows up when the code runs. A few examples (types as of @types/lodash 4.17.25):
-
Nullable inputs.
_.map(maybeArray, fn)acceptsnumber[] | undefinedand returns[]at runtime, so missing data flows on silently. Remeda rejects the call until theundefinedis handled. -
Property names as strings.
_.sortBy(users, "nmae")and_.pick(user, ["nmae"])both compile. At runtime the first returns the list unsorted and the second returns an object without the field. In Remeda both are compile errors: keys are checked against the object type and iteratees are functions, so a typo is caught where it is written. -
Missing groups.
_.groupBy(users, (u) => u.role).adminis typedUser[](unless the project enablesnoUncheckedIndexedAccess), so.admin.lengththrows when there are no admins. Remeda returnsPartial<Record<K, NonEmptyArray<T>>>, so the lookup is[User, ...User[]] | undefinedand the missing case has to be handled before the code compiles. -
Narrowing survives. Anything already established about the data, such as a non-empty check or a tuple shape, is carried through the utilities instead of being widened away. After
hasAtLeast(ids, 1),first(sortBy(ids, (id) => id))isnumber. Lodash'ssortByreturnsnumber[], so_.firstisnumber | undefinedagain and the code either repeats the check or adds a!, which is a runtime error waiting to happen. The same holds for tuples:map,filter,drop,zipand friends keep the tuple shape, sofilter([1, "a", 2, "b"] as const, isNumber)is typed[1, 2]where Lodash givesnumber[].
On top of that it is tree-shakable ESM with both data-first and data-last calling styles, and pipe keeps the same inference going across a chain.
Against the CONTRIBUTING criteria: actively maintained, well above the download threshold, and there is a per-function migration guide at https://remedajs.com/migrate/lodash that maps each Lodash function to a Remeda equivalent or a native alternative and calls out the typing and semantic differences.
Scope: add remeda to the lodash, lodash-es and underscore mappings only, plus a section on the page, following the moment precedent of listing several alternatives with different trade-offs. The per-method lodash.* mappings stay as they are. Remeda has no compat layer, so the module-level entry is the only one that is accurate for every function.
Availability
n/a (package replacement)
Code example (optional)
import _ from "lodash";
import { filter, first, groupBy, hasAtLeast, isNumber, map, sortBy } from "remeda";
declare const users: { name: string; role: "admin" | "member" }[];
declare const maybeNumbers: number[] | undefined;
declare const ids: number[];
declare const tagged: [1, "a", 2, "b"];
// Lodash: compiles, returns [] at runtime
_.map(maybeNumbers, (n) => n * 2);
// Remeda: error, 'number[] | undefined' is not assignable
map(maybeNumbers, (n) => n * 2);
// Lodash: compiles, returns the list unsorted at runtime
_.sortBy(users, "nmae");
// Remeda: error, property 'nmae' does not exist
sortBy(users, (u) => u.nmae);
// Lodash: typed User[], throws at runtime when there are no admins
_.groupBy(users, (u) => u.role).admin.length;
// Remeda: typed [User, ...User[]] | undefined, error until handled
groupBy(users, (u) => u.role).admin.length;
if (hasAtLeast(ids, 1)) {
// Lodash: number | undefined, the non-empty check is lost after sortBy
_.first(_.sortBy(ids, (id) => id));
// Remeda: number, sortBy keeps the array non-empty
first(sortBy(ids, (id) => id));
}
// Lodash: number[], the tuple and its literals are gone
_.filter(tagged, _.isNumber);
// Remeda: [1, 2]
filter(tagged, isNumber);
Activity
[-][Replacement]: lodash[/-][+][Replacement]: `lodash`[/+]gameroman commentedon Oct 20, 2026
it has double the install size of lodash
43081j commentedon Oct 21, 2026
It seems most of that comes from source maps. If you turn minification off, you don't need those anymore. It'll be a lot smaller and minification is the user's responsibility anyway.
fix(build): publish unminified code without source maps (#1465)
eranhirsch commentedon Oct 21, 2026
I've turned off minification and removed the source maps in remeda/remeda/1465, the package is now 1.44MB (exactly one floppy 💾 !)
Remeda ships its own types, so for TypeScript projects the lodash + @types/lodash row is the fairer comparison.
On how much weight install size should carry here: es-toolkit, already listed for these modules, was 2.51 MB when it was added in #259, about 1.8× lodash. Today it’s 3× lodash. If install size is a criterion for the lodash mappings, it should apply to both entries. If it isn’t, it shouldn’t block this one.
eranhirsch commentedon Oct 25, 2026
So what are the next steps? can we move forward with the attached PR? Is there anything on my side you need from me in order to make progress?