Skip to content

[Replacement]: lodash #1123

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

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