cheat sheet

lodash

Package-level reference for lodash on npm — modular imports, ES2020+ replacements, lodash/fp, debounce/throttle, and the honest 'you might not need this' framing.

lodash

What it is

lodash is the most-downloaded utility library on npm — ~50 million weekly downloads — providing ~300 helpers for arrays, objects, strings, functions, and collections. It was authored by John-David Dalton in 2012 as a faster, modular fork of underscore.js, and became the de-facto utility belt of the entire Node.js ecosystem.

It exposes both a chainable wrapper (_(arr).map(...).filter(...).value()) and standalone functions (_.map(arr, ...)). The core selling points are deep-clone, deep-merge, deep-equal, get/set with path strings, debounce/throttle, groupBy, keyBy, pick/omit, and cloneDeep — utilities that ES2020+ JavaScript still doesn't have built-in, despite many simpler lodash functions now being obsolete.

The honest framing in 2026: half of lodash's surface area is now redundant. Array.prototype.flat, Array.prototype.flatMap, Object.fromEntries, Array.from, optional chaining (?.), nullish coalescing (??), and structuredClone() cover what _.flatten, _.flatMapDeep, _.fromPairs, _.toArray, _.get (sometimes), and _.cloneDeep (sometimes) used to do. But the other half — debounce, throttle, groupBy for older Node, deep-merge with custom resolvers, mapKeys, partition — still has no platform equivalent that matches the ergonomics.

Install

bash
# npm / pnpm / yarn / bun
npm install lodash
pnpm add lodash
yarn add lodash
bun add lodash

Output: runtime dep — lodash ships in your prod bundle. Full UMD bundle is ~25 KB gzipped.

bash
# TypeScript types are NOT bundled — install separately
npm install --save-dev @types/lodash

Output: dev dep — DefinitelyTyped declarations from the community.

bash
# The "fp" variant — auto-curried, data-last, immutable
npm install lodash
import _ from "lodash/fp";   // same package, different entry

Output: functional flavour — _.map(fn, coll) instead of _.map(coll, fn); safer in pipelines and tree-shakes better.

bash
# Per-method packages (one function = one package)
npm install lodash.debounce lodash.throttle lodash.clonedeep

Output: legacy single-purpose packages. Used to be the way to dodge bundle bloat. Mostly obsolete now — modern bundlers tree-shake lodash-es.

bash
# ESM-native fork — strongly preferred for bundlers
npm install lodash-es

Output: same API; published as ESM with sideEffects: false so Webpack/Vite/Rollup tree-shake unused functions. Use this in any bundled app.

Versioning & Node support

  • Current major line is 4.x (stable since 2016 — yes, ten years). The API surface has barely changed; lodash is the ultimate "if it ain't broke" library. Many CVEs have been patched; minor versions have shipped throughout 2025.
  • Pure JS with optional @types/lodash declarations; runs on any modern runtime — Node 14+, Bun, Deno (via npm specifier), Cloudflare Workers, browsers (ES5+ baseline for the main build, ES2015+ for lodash-es).
  • The main lodash package is CJS by default; lodash-es is ESM. Both publish from the same source.
  • Always a runtime dependency — your prod code calls the helpers.
  • A 5.x line has been on the roadmap for years and has not shipped. Don't wait for it.

Package metadata

  • Maintainer: John-David Dalton (@jdalton) + the lodash org
  • Project home: github.com/lodash/lodash
  • Docs: lodash.com/docs
  • npm: npmjs.com/package/lodash
  • License: MIT
  • First released: April 2012
  • Downloads: ~50 million per week — by some counts the most-installed package on npm

Peer dependencies & extras

Lodash is zero-dependency. Common companions:

PackagePurpose
@types/lodashTypeScript declarations (DefinitelyTyped). Required for any TS project using lodash.
@types/lodash-esTS declarations for the ESM build.
lodash-esESM-published mirror — preferred for any bundled web app.
lodash.<method>Single-method packages (e.g. lodash.debounce). Mostly obsolete; use lodash-es instead.
lodash/fpFunctional flavour — same package, different entry point.
babel-plugin-lodashRewrites import _ from "lodash" into per-method imports. Largely obsolete now that lodash-es exists.

Alternatives

LibraryTrade-off
Native ES2020+Half of lodash is now in the platform — .flat, .flatMap, Object.fromEntries, structuredClone, ?., ??. Free, zero bundle. Pick first.
RamdaFunctional-first, auto-curried, data-last (like lodash/fp from day one). Smaller surface area; stricter immutability. Pick for FP-heavy codebases.
remedaTypeScript-first lodash alternative with better type inference. Modular, ESM-native, smaller bundles. Growing fast in 2025-2026.
es-toolkitNew ~3KB gzipped lodash alternative from Toss; modern-first, no IE shims. Drop-in for ~30 common helpers.
rambdaxRamda with extras. Same trade-off as Ramda.
just-*The just- family (just-debounce-it, just-clone, just-pick) — single-purpose, zero-dep, modern-only. Pick when you need 2-3 helpers and don't want lodash.

Common gotchas

  1. CJS lodash doesn't tree-shake. import _ from "lodash" in a bundler pulls the entire ~25 KB bundle, even if you only call _.debounce. Use lodash-es (import { debounce } from "lodash-es") or per-method imports (import debounce from "lodash/debounce").
  2. _.get(obj, "a.b.c") is now obj?.a?.b?.c. Optional chaining covers ~90% of _.get use cases. Keep _.get for dynamic path strings only.
  3. _.cloneDeep is slower than structuredClone. Native structuredClone(obj) (Node 17+, all modern browsers) is 5-10× faster for plain data. lodash still wins for objects with functions, DOM nodes, or class instances — but those rarely round-trip cleanly anyway.
  4. _.isEqual is structural, not by reference. Two objects with the same shape but different prototypes can compare equal. For class-instance equality, write your own.
  5. _.merge mutates the first argument. _.merge(a, b) modifies a in place and returns it. Use _.merge({}, a, b) to avoid surprising side effects.
  6. _.debounce keeps a timer reference — call .cancel() on unmount. In React, an un-cancelled debounce can fire after the component unmounted, triggering state updates on dead components.

Real-world recipes

The patterns that come up in nearly every codebase still using lodash in 2026.

pick / omit / get / set — path-string object surgery

typescript
import { pick, omit, get, set } from "lodash-es";

const user = { id: 1, name: "Alice", email: "alice@example.com", password: "secret" };

const publicView = pick(user, ["id", "name", "email"]);
const safeUser = omit(user, ["password"]);
const email = get(user, "email", "no-email@example.com");

const config = {};
set(config, "db.connection.host", "localhost");
set(config, "db.connection.port", 5432);

Output:

text
publicView = { id: 1, name: "Alice", email: "alice@example.com" }
safeUser   = { id: 1, name: "Alice", email: "alice@example.com" }
email      = "alice@example.com"
config     = { db: { connection: { host: "localhost", port: 5432 } } }

Native equivalents exist but aren't as ergonomic — pick becomes Object.fromEntries(Object.entries(user).filter(([k]) => keys.includes(k))), which is harder to read. lodash's path-string API still wins for dynamic field lists.

Deep clone with cloneDeep — when structuredClone isn't enough

typescript
import { cloneDeep } from "lodash-es";

class Money { constructor(public amount: number, public currency: string) {} }

const order = {
  id: 42,
  total: new Money(99.99, "USD"),
  items: [{ sku: "A1" }, { sku: "B2" }],
  callback: () => console.log("paid"),
};

const copy = cloneDeep(order);
copy.items[0].sku = "MODIFIED";

console.log(order.items[0].sku);   // "A1" — untouched
console.log(copy.total instanceof Money);   // true — class preserved

Output: cloneDeep preserves class instances, symbols, and functions; structuredClone throws on functions and class instances. Use structuredClone for plain data, cloneDeep for everything else.

debounce + throttle — rate-limiting callbacks

typescript
import { debounce, throttle } from "lodash-es";

// Debounce: fire after `wait` ms of silence
const onSearch = debounce(async (query: string) => {
  const results = await fetch(`/api/search?q=${query}`).then(r => r.json());
  renderResults(results);
}, 300);

// Throttle: fire at most once per `wait` ms
const onScroll = throttle(() => {
  updateScrollPosition(window.scrollY);
}, 100);

window.addEventListener("scroll", onScroll);

// In React useEffect cleanup
useEffect(() => {
  return () => onSearch.cancel();   // cancel pending invocation
}, []);

Output: debounce waits for the storm to end; throttle samples at intervals. Both are still standout lodash exports — no clean platform equivalent exists.

groupBy + countBy — collection bucketing

typescript
import { groupBy, countBy } from "lodash-es";

const orders = [
  { id: 1, status: "shipped", total: 49 },
  { id: 2, status: "pending", total: 99 },
  { id: 3, status: "shipped", total: 19 },
  { id: 4, status: "cancelled", total: 5 },
];

const byStatus = groupBy(orders, "status");
const counts = countBy(orders, "status");

Output:

json
{
  "shipped":   [{ "id": 1, "status": "shipped", "total": 49 }, { "id": 3, ... }],
  "pending":   [{ "id": 2, "status": "pending", "total": 99 }],
  "cancelled": [{ "id": 4, "status": "cancelled", "total": 5 }]
}

Object.groupBy shipped in Node 21+ and modern browsers — for new code on supported runtimes, use the native version. countBy has no native equivalent yet.

map + chain — pipeline composition

typescript
import _ from "lodash-es";

const result = _.chain(orders)
  .filter({ status: "shipped" })
  .map((o) => ({ ...o, total: o.total * 1.1 }))   // 10% tax
  .sumBy("total")
  .value();

// lodash/fp equivalent — pipeline-friendly, no .value() needed
import { flow, filter, map, sumBy } from "lodash/fp";

const result2 = flow(
  filter({ status: "shipped" }),
  map((o: Order) => ({ ...o, total: o.total * 1.1 })),
  sumBy("total"),
)(orders);

Output: _.chain is the OO-style pipeline; lodash/fp + flow is the FP-style. The fp flavour tree-shakes better and composes cleanly with TypeScript inference improvements in 2025+.

mapKeys + invert — quick key renames

typescript
import { mapKeys, invert, camelCase } from "lodash-es";

const apiResponse = { user_id: 1, full_name: "Alice", created_at: "2026-05-31" };

const camelized = mapKeys(apiResponse, (_v, k) => camelCase(k));
// { userId: 1, fullName: "Alice", createdAt: "2026-05-31" }

const codeToLabel = { US: "United States", GB: "United Kingdom" };
const labelToCode = invert(codeToLabel);
// { "United States": "US", "United Kingdom": "GB" }

Output: still one of the cleanest ways to mass-rename keys in a payload. Native equivalent is Object.fromEntries(Object.entries(...).map(...)) — verbose.

Production deployment

Lodash is a runtime dep; the deployment concerns are bundle size, tree-shaking, and CVE drift.

Pick the right entry point

ScenarioImport
Bundled web app (Vite, Webpack 5+, Rollup, esbuild)import { debounce } from "lodash-es" — tree-shakes
Node-only CLIimport _ from "lodash" — full bundle, fine on the server
Tiny browser bundle, 2-3 helpersimport debounce from "just-debounce-it" (or es-toolkit, remeda)
TypeScript projectAlways npm install --save-dev @types/lodash (or @types/lodash-es)

Bundle size measurements

Import formResult (gzipped)
import _ from "lodash"~25 KB
import _ from "lodash-es" with tree-shakingonly what you import — ~2 KB per helper
import debounce from "lodash/debounce"~3 KB
import { debounce } from "lodash.debounce"~2 KB
Native Object.groupBy(...) etc.0 KB

CVE drift

lodash has had a steady trickle of CVEs (zipObjectDeep, set, merge prototype-pollution chains; see Security below). Dependabot keeps the patch line current — pin ^4.17.21 or newer. Don't trust any lodash older than 2021.

Performance tuning

Native first

typescript
// Slow: lodash deep equal across 10k items
arr.filter((a) => _.isEqual(a, target));   // ~500ms

// Fast: native + structural comparison via JSON.stringify (or stable-stringify)
const targetKey = JSON.stringify(target);
arr.filter((a) => JSON.stringify(a) === targetKey);   // ~50ms

For hot loops, native operators are 5-50× faster than lodash. Reach for lodash for readability, not performance.

cloneDeep is expensive

cloneDeep on a 100 KB object can take 5-20ms. Avoid in hot paths; freeze the original and pass references where possible. structuredClone is 5-10× faster but doesn't handle functions.

chain is slower than flow

_.chain(arr).map(...).filter(...).value() allocates a wrapper object plus a closure per step. lodash/fp + flow is materially faster and tree-shakes — prefer it for new code.

Memoise expensive selectors

typescript
import { memoize } from "lodash-es";

const expensiveFn = memoize((input: string) => doSlowThing(input));

_.memoize keys on the first argument by default (use .cache.set() or pass a resolver). Fine for pure functions; broken for functions with multiple meaningful arguments — pass an explicit resolver.

Version migration guide

Lodash 4.x has been stable since 2016. There's no live "v5" to migrate to — but several quality-of-life migrations are worth doing.

typescript
// Before
import _ from "lodash";
const debounced = _.debounce(fn, 300);

// After
import { debounce } from "lodash-es";
const debounced = debounce(fn, 300);

Output: same runtime behaviour; bundler tree-shakes unused helpers. Saves 10-20 KB gzipped typically.

lodashNative replacement
_.get(obj, "a.b.c")obj?.a?.b?.c
_.isNil(x)x == null
_.flatten(arr)arr.flat()
_.flatMap(arr, fn)arr.flatMap(fn)
_.fromPairs(entries)Object.fromEntries(entries)
_.toPairs(obj)Object.entries(obj)
_.cloneDeep(obj) (plain data)structuredClone(obj)
_.groupBy(arr, "key")Object.groupBy(arr, x => x.key) (Node 21+)
_.uniq(arr)[...new Set(arr)]

Keep debounce, throttle, cloneDeep (for non-plain data), pick/omit (dynamic keys), mapKeys, partition, keyBy, countBy.

lodash/fp migration

typescript
// OO style — chainable
_.chain(arr).map(fn).filter(pred).value();

// FP style — composable
import { flow, map, filter } from "lodash/fp";
flow(map(fn), filter(pred))(arr);

lodash/fp is auto-curried and data-last — composes cleanly into pipelines. Tree-shakes better than chain.

Security considerations

Lodash's CVE history is real — almost every one is prototype-pollution via deep-set helpers.

CVEFunctionYearFixed in
CVE-2018-16487defaultsDeep, merge20184.17.11
CVE-2019-10744defaultsDeep20194.17.12
CVE-2020-8203zipObjectDeep, set, setWith20204.17.19
CVE-2021-23337template20214.17.21
Variousset, setWith2020-20224.17.21+

Rules:

  1. Pin lodash to ^4.17.21 or newer. Older versions are exploitable via prototype-pollution if you pass user input to _.set, _.merge, _.defaultsDeep, or _.zipObjectDeep.
  2. Never _.merge(target, userInput) or _.set(target, userPath, val). Even on the patched version, design the data flow so user input never feeds path strings or deep-merge targets.
  3. _.template is eval. It executes user-provided template strings. Don't use with untrusted input.
  4. Audit transitive deps. Many older libraries pin lodash@^4.17.5 (pre-patch). npm audit and Dependabot catch these; bump or yarn resolutions/npm overrides as needed.

Testing & CI integration

typescript
import { describe, it, expect } from "vitest";
import { groupBy, cloneDeep } from "lodash-es";

describe("groupBy", () => {
  it("buckets by key string", () => {
    const result = groupBy([{ t: "a" }, { t: "b" }, { t: "a" }], "t");
    expect(result).toEqual({ a: [{ t: "a" }, { t: "a" }], b: [{ t: "b" }] });
  });
});

describe("cloneDeep", () => {
  it("preserves nested structure", () => {
    const src = { a: { b: { c: 1 } } };
    const copy = cloneDeep(src);
    copy.a.b.c = 99;
    expect(src.a.b.c).toBe(1);
  });
});

Output: lodash functions are pure (mostly) — easy to unit-test. Test your wrappers, not lodash itself.

For CI:

yaml
# .github/workflows/ci.yml
- run: npm audit --production --audit-level=high

Catches new lodash CVEs before they ship.

Ecosystem integrations

ToolIntegration
reactdebounce/throttle in event handlers; cloneDeep for state snapshots
reduxcloneDeep historically; modern code uses Immer instead
webpack / viteUse lodash-es for tree-shaking
babel-plugin-lodashAuto-rewrites import _ from "lodash" into per-method imports (mostly obsolete)
babel-plugin-transform-importsSame idea, more general
eslint-plugin-lodash-fpLints lodash/fp usage for purity violations
@types/lodashTypeScript declarations

Troubleshooting common errors

  • Module not found: 'lodash-es' — install separately (npm install lodash-es). It's not in lodash.
  • Cannot find module 'lodash/debounce' — TypeScript compilation. Install @types/lodash.
  • Bundle size explodes after adding lodash — using CJS lodash in a bundler. Switch to lodash-es or per-method imports.
  • _.merge is mutating my object — that's the documented behaviour. Use _.merge({}, a, b).
  • debounce never fires in React — created inside a render. Use useMemo(() => debounce(fn, 300), []) or useRef.
  • _.isEqual returns false on what looks identical — class instances with different prototypes, or one has a Symbol key. Check getPrototypeOf / Reflect.ownKeys.
  • Audit warns about prototype pollution — upgrade to ^4.17.21 or newer; switch to lodash-es if not already.

When NOT to use this

  • You're starting a new project in 2026. Reach for native ES2020+ first. Only add lodash when you hit a concrete need (debounce, cloneDeep for non-plain data, groupBy on older Node, dynamic-path get/set).
  • Bundle-size-critical web app. Use es-toolkit (~3KB) or remeda (~5KB) instead. Both cover ~30 of the most-common lodash helpers with better TypeScript inference.
  • You only need 1-2 helpers. npm install just-debounce-it or npm install just-clone — single-purpose, zero-dep, ~1KB each.
  • You're chaining with Ramda or rxjs. Use their pipe operators; mixing paradigms hurts readability.
  • Plain JSON.parse(JSON.stringify(x)) is enough. For trusted plain data, the JSON round-trip clones in one line and outperforms cloneDeep. (Still slower than structuredClone for plain data, but no dependency required.)
  • Modern React + Redux Toolkit. RTK uses Immer for immutable updates — don't pile on lodash too.

See also