Skip to content

[Replacement]: lodash #1123

Open
Open

Description

@eranhirsch

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);

Activity

  1. changed the title [-][Replacement]: lodash[/-] [+][Replacement]: `lodash`[/+] on Oct 15, 2026
  2. gameroman commented on Oct 20, 2026

    @gameroman
    Contributor

    it has double the install size of lodash

  3. 43081j commented on Oct 21, 2026

    @43081j
    Contributor

    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.

  4. eranhirsch commented on Oct 21, 2026

    @eranhirsch
    Author

    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.

    Package Install size
    remeda@2.52.0 2.90 MB
    remeda@X.Y.0 1.44 MB
    lodash@4.18.1 1.41 MB
    lodash + @types/lodash 2.28 MB
    es-toolkit@1.52.0 4.23 MB
  5. eranhirsch commented on Oct 25, 2026

    @eranhirsch
    Author

    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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions