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) accepts number[] | undefined and returns [] at runtime, so missing data flows on silently. Remeda rejects the call until the undefined is 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).admin is typed User[] (unless the project enables noUncheckedIndexedAccess), so .admin.length throws when there are no admins. Remeda returns Partial<Record<K, NonEmptyArray<T>>>, so the lookup is [User, ...User[]] | undefined and 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)) is number. Lodash's sortBy returns number[], so _.first is number | undefined again 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, zip and friends keep the tuple shape, so filter([1, "a", 2, "b"] as const, isNumber) is typed [1, 2] where Lodash gives number[].
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);
Package to replace
lodash(together withlodash-esandunderscore, which share thelodash-underscorepage)Suggested replacement(s)
remedaManifest 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/lodash4.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
pipekeeps 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
remedato thelodash,lodash-esandunderscoremappings only, plus a section on the page, following themomentprecedent of listing several alternatives with different trade-offs. The per-methodlodash.*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)