Migrate: lodash
Array
difference
In Remeda difference
treats the inputs as multisets/bags which respect
item duplication, whereas in Lodash all matching values are filtered out.
This only matters for cases where both arrays might have duplicate values. Use
filter
, isNot
, and
isIncludedIn
instead to compose the same logic.
No duplicates
// Lodash
difference([2, 1], [2, 3]);
// Remeda
difference([2, 1], [2, 3]);
With duplicates
// Lodash
difference([1, 1, 2, 2], [1]);
// Remeda
filter([1, 1, 2, 2], isNot(isIncludedIn([1])));
flat
Either use with no params, or with a depth of 1.
// Lodash
flatten(DATA);
// Remeda
flat(DATA);
flat(DATA, 1 /* depth */);
flat
Unlike flatten
in Lodash, the Remeda flat
function is always bound by the
depth
param. To replicate the Lodash behavior use a high, const value.
Infinity
and Number.MAX_INTEGER
are not consts and would result in
inefficient typing.
// Lodash
flattenDeep(DATA);
// Remeda
flat(DATA, 100); // ✅
flat(DATA, Infinity); // ❌
intersection
In Remeda intersection
treats the inputs as multisets/bags which respect
item duplication, whereas in Lodash the result is always de-dupped. This only
matters for cases where both arrays might have duplicate values. To achieve
the same output pass the result to unique
.
No duplicates
// Lodash
intersection([2, 1], [2, 3]);
// Remeda
intersection([2, 1], [2, 3]);
With duplicates
// Lodash
intersection([1, 1], [1, 1]);
// Remeda
unique(intersection([1, 1], [1, 1]));
Not provided by Remeda.
- For index
0
usefirst
. - For index
-1
uselast
. - For arbitrary non-negative indices use the native JS
data[n]
. - Or use
Array.prototype.at
for any index.
// Lodash
nth(DATA);
nth(DATA, 0);
// Remeda
first(DATA);
// Lodash
nth(DATA, 1);
// Native
DATA[1];
DATA.at(1);
// Lodash
nth(DATA, -1);
// Remeda
last(DATA);
// Lodash
nth(DATA, -2);
// Native
DATA.at(-2);
reverse
Lodash mutates the input but in Remeda a new array is returned instead.
// Lodash
const DATA = [1, 2, 3];
reverse(DATA);
// Remeda
let DATA = [1, 2, 3];
DATA = reverse(DATA);
unique
Not provided by Remeda.
There is no equivalent function in Remeda because both unique
and sortedUniq
run at O(n) time complexity. In space complexity (the amount of memory
needed to compute them), unique
is O(k) where k is the number of unique
values in the array, while sortedUniq
is O(1). In most real-life use cases,
the two implementations would have near-identical performance. We highly
recommend always using unique
unless you know that the difference matters.
If you still need this algorithm, use filter
with the
following predicate:
// Lodash
sortedUniq(DATA);
// Remeda
unique(DATA);
filter(DATA, (item, index, array) => index === 0 || item !== array[index - 1]);
Not provided by Remeda.
Compose zip
and fromEntries
:
// Lodash
zipObject(keys, values);
// Remeda
fromEntries(zip(keys, values));
Collection
Not provided by Remeda.
Use the native JS Array.prototype.every
instead.
// Lodash
every(DATA, predicate);
// Native
DATA.every(predicate);
Not provided by Remeda.
This function could be recreated in Remeda via a composition of
map
, and flat
with a large, const depth
parameter.
// Lodash
flatMapDeep(DATA, mappingFunction);
// Remeda
flat(map(DATA, mappingFunction), 10 /* depth */);
// Or as a pipe
pipe(DATA, map(mappingFunction), flat(10 /* depth */));
Not provided by Remeda.
This function could be recreated in Remeda via map
,
flatMap
, or flat
(or a combination of them,
depending on the parameters used).
// Lodash
flatMapDepth(DATA);
// Remeda
flat(DATA);
// Lodash
flatMapDepth(DATA, mappingFunction);
// Remeda
flatMap(DATA, mappingFunction);
// Lodash
flatMapDepth(DATA, mappingFunction, depth);
// Remeda
flat(map(DATA, mappingFunction), depth);
// Or as a pipe
pipe(DATA, map(mappingFunction), flat(depth));
forEach
- In Lodash
forEach
first checks if the data argument is array-like (by looking for alength
prop). In Remeda the function only accepts arrays and calling it with an object would result in a TypeScript error. UseforEachObj
instead for those cases. - In Lodash
forEach
stops iterating early when the callback returnsfalse
. In Remeda the callback cannot return a value (it must returnvoid
) and this feature isn’t available. You can build apipe
that would replicate this logic.
Array
// Lodash
forEach([1, 2, 3], (item) => console.log(item));
// Remeda
forEach([1, 2, 3], (item) => {
console.log(item);
});
Object
// Lodash
forEach({ a: 1, b: 2, c: 3 }, (value) => console.log(value));
// Remeda
forEachObj({ a: 1, b: 2, c: 3 }, (value) => {
console.log(value);
});
Early exit
// Lodash
forEach([1, 2, 3], (item) => (item === 2 ? false : console.log(item)));
// Remeda
// ❌ You can't return a value in a forEach callback in Remeda:
forEach([1, 2, 3], (item) => (item === 2 ? false : console.log(item)));
// ✅ Instead you can rely on lazy evaluation of pipes:
pipe(
[1, 2, 3],
takeWhile((item) => item !== 2),
forEach((item) => {
console.log(item);
}),
);
Not provided by Remeda.
- This function could be replicated using a native JS
for
loop. - If the order of iteration doesn’t matter, you can use
forEach
for arrays, orforEachObj
for objects. - The order in which object properties are iterated over is well-defined, but
might not be the order you expect (MDN); nevertheless,
entries
maintains the same order, and could be used to replicateforEachRight
. - If the native solution doesn’t suffice please open an issue at Remeda’s GitHub project so we can learn about your use-case.
Arrays
const DATA = [1, 2, 3];
// Lodash
forEachRight(DATA, (item) => (item === 2 ? false : console.log(item)));
// Native
for (const index = DATA.length - 1; index >= 0; index--) {
const item = DATA[item]!;
if (item === 2) {
break;
}
console.log(item);
}
Objects
const DATA = { a: 1, b: 2, c: 3 };
// Lodash
forEachRight(DATA, (value) => (value === 2 ? false : console.log(value)));
// Native
const keys = Object.keys(DATA);
for (const index = keys.length - 1; index >= 0; index--) {
const value = DATA[key]!;
if (value === 2) {
break;
}
console.log(value);
}
isIncludedIn
- In Lodash
includes
also works on strings and objects; In Remeda only arrays are supported. For objects, usevalues
first. For strings prefer the native JSString.prototype.includes
. - The 3rd optional parameter
fromIndex
isn’t supported in Remeda. You can replicate it’s usage withdrop
(for non-negative indices), ortakeLast
(for non-positive indices). - The order of the parameters is flipped in Remeda, the item is first, and then the array.
Arrays
// Lodash
includes([1, 2, 3], 1);
// Remeda
isIncludedIn(1, [1, 2, 3]);
Objects
// Lodash
includes({ a: 1, b: 2 }, 1);
// Remeda
isIncludedIn(1, values({ a: 1, b: 2 }));
Strings
const DATA = "Hello, World!";
// Lodash
includes(DATA, "lo");
// Native
DATA.includes("lo");
fromIndex: non-negative
// Lodash
includes([1, 2, 3], 1, 2);
// Remeda
isIncludedIn(1, drop([1, 2, 3], 2));
fromIndex: non-positive
// Lodash
includes([1, 2, 3], 1, -2);
// Remeda
isIncludedIn(1, takeLast([1, 2, 3], 2));
map
map
can be used instead ofinvokeMap
by using a callback functions instead of (string) function names.- If you need to invoke functions dynamically via a provided function name, narrow the name first (so you can handle errors explicitly), and then use the name to access the object.
No arguments
// Lodash
invokeMap(
[
[5, 1, 7],
[3, 2, 1],
],
"sort",
);
// Remeda
map(
[
[5, 1, 7],
[3, 2, 1],
],
(array) => array.sort(),
);
With arguments
// Lodash
invokeMap([123, 456], String.prototype.split, "");
// Remeda
map([123, 456], (num) => num.toString().split(""));
Dynamic
const DATA = [
{ foo: (x: number) => x + 1, bar: (x: number) => x - 1 },
{ foo: (x: number) => x * 2, bar: (x: number) => x / 2 },
] as const;
// Lodash
invokeMap(DATA, funcName, 3);
// Remeda
if (funcName === "foo" || funcName === "bar") {
map(DATA, ({ [funcName]: func }) => func(3));
} else {
// Error!
}
// Or
map(DATA, (obj) => {
if (funcName === "foo" || funcName === "bar") {
return obj[funcName](3);
} else {
// Error
}
});
sortBy
Unlike in Lodash, in Remeda the sort criteria can only be a callback (and not a prop name), it is paired with the order modifier (“asc”/“desc”, if needed), and it’s provided as a variadic argument and not two array arguments.
const DATA = [
{ user: "fred", age: 48 },
{ user: "barney", age: 34 },
{ user: "fred", age: 40 },
{ user: "barney", age: 36 },
];
// Lodash
orderBy(DATA, ["user", "age"], ["asc", "desc"]);
// Remeda
sortBy(DATA, prop("user"), [prop("age"), "desc"]);
sample
- The
sample
function in Remeda takes a required second parameter of the size of the sample. Use 1. - The Remeda function returns an array; to extract the value either use
native destructuring or index access, or use
only
. - For objects, first call
values
. - If you are using
sample
in an extremely hot path where you need the most efficient implementation possible, prefer a native implementation instead.
Simple
// Lodash
sample(DATA);
// Remeda
only(sample(DATA, 1 /* size */));
// or with a pipe
pipe(DATA, sample(1 /* size */), only());
// Or with native accessors
sample(DATA, 1 /* size */)[0];
sample(DATA, 1 /* size */).at(0)!;
const [result] = sample(DATA, 1 /* size */);
Native
// Lodash
sample(DATA);
// Native
DATA[Math.floor(Math.random() * DATA.length)]!;
DATA.at(Math.floor(Math.random() * DATA.length))!;
sample
- Unlike Lodash, In Remeda the
sample
function returns items in the same order they appear in the input. Use theshuffle
function if you also need to randomize their order. - The second parameter to
sampleSize
in Lodash is optional, and defaults to 1 when not provided. In Remeda this is a required param.
// Lodash
sampleSize(DATA);
// Remeda
sample(DATA, 1);
// Lodash
sampleSize(DATA, 2);
// Remeda
// ❌ The result isn't shuffled!
sample(DATA, 2);
// ✅ Add `shuffle` after `sample` (if order is important).
shuffle(sample(DATA, 2));
// Or with a pipe
pipe(DATA, sample(2), shuffle());
Not provided by Remeda.
Use the native JS Array.prototype.some
instead.
// Lodash
some(DATA, predicate);
// Native
DATA.some(predicate);
sortBy
The sortBy
function in Remeda does not support defining the sort criteria
using prop names, only callback functions. Also, the sort criteria is provided
as a variadic argument of sort criteria, and not a single array argument.
const DATA = [
{ user: "fred", age: 48 },
{ user: "barney", age: 34 },
{ user: "fred", age: 40 },
{ user: "barney", age: 36 },
];
// Lodash
sortBy(DATA, [
function (o) {
return o.user;
},
]);
sortBy(DATA, ["user", "age"]);
// Remeda
sortBy(DATA, prop("user"));
sortBy(DATA, prop("user"), prop("age"));
Function
funnel
-
debounce
can be implemented using thefunnel
utility. A reference implementation is provided below, and a more expanded version with inline documentation and tests is available in the test filefunnel.lodash-debounce.test.ts
. -
A more complete reference implementation that also maintains Lodash’s capability to store the callback’s return value is available below, and in
funnel.lodash-debounce-with-cached-value.test.ts
. -
These implementations can be copied as-is into your project, but might contain redundant parts which are not relevant for your specific use cases. By inlining only the parts you need you can take advantage of capabilities not available in Lodash.
Reference
function debounce<F extends (...args: any) => void>(
func: F,
wait = 0,
{
leading = false,
trailing = true,
maxWait,
}: {
readonly leading?: boolean;
readonly trailing?: boolean;
readonly maxWait?: number;
} = {},
) {
const {
call,
isIdle: _isIdle,
...rest
} = funnel(
() => {
if (leading || trailing) {
func();
}
},
{
minQuietPeriodMs: wait,
...(maxWait !== undefined && { maxBurstDurationMs: maxWait }),
triggerAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(call, rest);
}
With call arguments
function debounce<F extends (...args: any) => void>(
func: F,
wait = 0,
{
leading = false,
trailing = true,
maxWait,
}: {
readonly leading?: boolean;
readonly trailing?: boolean;
readonly maxWait?: number;
} = {},
) {
const {
call,
isIdle: _isIdle,
...rest
} = funnel(
(args: Parameters<F>) => {
if (leading || trailing) {
func(...args);
}
},
{
reducer: (_, ...args: Parameters<F>) => args,
minQuietPeriodMs: wait,
...(maxWait !== undefined && { maxBurstDurationMs: maxWait }),
triggerAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(call, rest);
}
With cached value
function debounce<F extends (...args: any) => any>(
func: F,
wait = 0,
{
leading = false,
trailing = true,
maxWait,
}: {
readonly leading?: boolean;
readonly trailing?: boolean;
readonly maxWait?: number;
} = {},
) {
let cachedValue: ReturnType<F> | undefined;
const { call, flush, cancel } = funnel(
(args: Parameters<F>) => {
if (leading || trailing) {
cachedValue = func(...args) as ReturnType<F>;
}
},
{
reducer: (_, ...args: Parameters<F>) => args,
minQuietPeriodMs: wait,
...(maxWait !== undefined && { maxBurstDurationMs: maxWait }),
triggerAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(
(...args: Parameters<F>) => {
call(...args);
return cachedValue;
},
{
flush: () => {
flush();
return cachedValue;
},
cancel,
},
);
}
partialBind
The placeholder argument _
is not supported. Some cases can be written as partialLastBind
; otherwise, use an arrow function.
function greet(greeting, firstName, lastName) {
return greeting + " " + firstName + " " + lastName;
}
// Lodash
_.partial(greet, _, "john", "doe");
_.partial(greet, "hi", _, "doe");
// Remeda
partialLastBind(greet, "john", "doe");
// (not supported)
// Native
(greeting) => greet(greeting, "john", "doe");
(firstName) => greet("hi", firstName, "doe");
partialLastBind
The placeholder argument _
is not supported. Some cases can be written as partialBind
; otherwise, use an arrow function.
function greet(greeting, firstName, lastName) {
return greeting + " " + firstName + " " + lastName;
}
// Lodash
_.partialRight(greet, "hi", "john", _);
_.partialRight(greet, "hi", _, "doe");
// Remeda
partialBind(greet, ["hi", "john"]);
// (not supported)
// Native
(lastName) => greet("hi", "john", lastName);
(firstName) => greet("hi", firstName, "doe");
funnel
-
throttle
can be implemented using thefunnel
utility. A reference implementation is provided below, and a more expanded version with inline documentation and tests is available in the test filefunnel.lodash-throttle.test.ts
. -
A more complete reference implementation that also maintains Lodash’s capability to store the callback’s return value is available below, and in
funnel.lodash-throttle-with-cached-value.test.ts
. -
These implementations can be copied as-is into your project, but might contain redundant parts which are not relevant for your specific use cases. By inlining only the parts you need you can take advantage of capabilities not available in Lodash.
Reference
function throttle<F extends (...args: any) => void>(
func: F,
wait = 0,
{
leading = true,
trailing = true,
}: { readonly leading?: boolean; readonly trailing?: boolean } = {},
) {
const {
call,
isIdle: _isIdle,
...rest
} = funnel(
() => {
if (leading || trailing) {
func();
}
},
{
burstCoolDownMs: wait,
maxBurstDurationMs: wait,
invokedAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(call, rest);
}
With call arguments
function throttle<F extends (...args: any) => void>(
func: F,
wait = 0,
{
leading = true,
trailing = true,
}: { readonly leading?: boolean; readonly trailing?: boolean } = {},
) {
const {
call,
isIdle: _isIdle,
...rest
} = funnel(
(args: Parameters<F>) => {
if (leading || trailing) {
func(...args);
}
},
{
reducer: (_, ...args: Parameters<F>) => args,
minQuietPeriodMs: wait,
maxBurstDurationMs: wait,
triggerAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(call, rest);
}
With cached value
function throttle<F extends (...args: any) => any>(
func: F,
wait = 0,
{
leading = true,
trailing = true,
}: { readonly leading?: boolean; readonly trailing?: boolean } = {},
) {
let cachedValue: ReturnType<F> | undefined;
const { call, flush, cancel } = funnel(
(args: Parameters<F>) => {
if (leading || trailing) {
cachedValue = func(...args) as ReturnType<F>;
}
},
{
reducer: (_, ...args: Parameters<F>) => args,
minQuietPeriodMs: wait,
maxBurstDurationMs: wait,
triggerAt: trailing ? (leading ? "both" : "end") : "start",
},
);
return Object.assign(
(...args: Parameters<F>) => {
call(...args);
return cachedValue;
},
{
flush: () => {
flush();
return cachedValue;
},
cancel,
},
);
}
Lang
Math
firstBy
Use "desc"
as the direction.
// Lodash
maxBy(DATA, criteria);
// Remeda
firstBy(DATA, [criteria, "desc"]);
Number
randomInteger
- When Lodash’s
random
function is called with a non-integer param it returns any number, not just integers (e.g. 2.5)! Remeda’srandomInteger
function always returns integers, effectively rounding the parameters to fit the range of possible integers. - If you want to generate any number in the range (and not just integers) see the solutions provided in “You don’t need Lodash”.
- Lodash’s
random
parameters are optional. In Remeda all parameters are required.
Two integer params
// Lodash
random(1, 10);
// Remeda
randomInteger(1, 10);
Single integer param
// Lodash
random(10);
// Remeda
randomInteger(0, 10);
No params
// Lodash
random();
// Remeda
randomInteger(0, 1);
Not supported: floating-point numbers
random(1.5, 3.5);
random(1.5);
random(10, true);
random(5, 10, true);
random(true);
Object
merge
- In Lodash the merge is done in-place, on the first argument to the function. In Remeda a new object is always returned, and none of the input objects are mutated.
- In Lodash
assign
can be used to merge any number of objects by accepting a variadic argument list. In Remeda, if you are merging only 2 objects usemerge
, but if you need to merge more than 2 objects usemergeAll
instead, which takes an Array of objects.
// Lodash
const DATA = { a: 1, b: 2 };
assign(DATA, b);
assign(DATA, b, c, d, e);
// Remeda
let DATA = { a: 1, b: 2 };
DATA = merge(DATA, b);
DATA = mergeAll([DATA, b, c, d, e]);
pathOr
- In Lodash the
get
function supports two ways of defining the path parameter: a string representation of the path (similar to XPath: e.g.a.b[0].c
), and an array representation of the path (e.g.['a', 'b', 0, 'c']
). In Remeda only the array representation is accepted. Use the helper functionstringToPath
to translate string paths to array paths. - In order to provide good types, the Remeda
pathOr
function is limited to paths of length 3 or less. Longer paths are not supported. prop
is a simplified version ofpathOr
for paths of length 1 andundefined
as the default (fallback) value.
const DATA = { a: [{ b: 123 }] };
// Lodash
get(DATA, "a");
get(DATA, ["a", 0, "b"], 456);
get(DATA, "a[0].b", 456);
// Remeda
prop(DATA, "a");
pathOr(DATA, ["a", 0, "b"], 456);
pathOr(DATA, stringToPath("a[0].b"), 456);
setPath
- In Lodash the
set
function supports two ways of defining the path parameter: a string representation of the path (similar to XPath: e.g.a.b[0].c
), and an array representation of the path (e.g.['a', 'b', 0, 'c']
). In Remeda only the array representation is accepted. Use the helper functionstringToPath
to translate string paths to array paths. - Unlike the Lodash
set
function, In Remeda the provided value must match the type of the prop at that path, the function does not support creating “sparse” objects. - For better type-safety, Remeda offers two additional functions to handle
paths of length 1. Use
set
to update an existing prop in an object (best type-safety and with IDE type-ahead), andaddProp
to add a new prop to the object (or to override its type) - In Lodash
set
mutates the input object. In Remeda a new object is returned instead. The input object is never mutated.
let data = { a: "hello", deep: [{ z: true }] };
// Lodash
set(data, "a", "world");
set(data, ["a"], "foo");
set(data, "a", 123);
set(data, ["a"], 456);
set(data, "b", 123);
set(data, ["b"], 456);
set(data, "deep[0].z", false);
set(data, ["deep", 0, "z"], false);
// ❌ Not supported:
set(data, "deep[0].newProp", 123);
set(data, ["deep", 0, "newProp"], 123);
// Remeda
data = set(data, "a", "world");
data = set(data, "a", "foo");
data = addProp(data, "a", 123);
data = addProp(data, "a", 456);
data = addProp(data, "b", 123);
data = addProp(data, "b", 456);
data = setPath(data, stringToPath("deep[0].z"), false);
data = setPath(data, ["deep", 0, "z"], false);
Seq
String
toCamelCase
- Camel-casing relies heavily on how “words” are broken up. Remeda uses the same
logic that
type-fest
uses. This might work slightly different from Lodash. - Remeda offers an optional flag for
toCamelCase
that changes how consecutive uppercase letters are handled. This flag is turned on by default, but the behavior is more similar to Lodash when it is turned off.
// Lodash
camelCase("HasHTML"); // "hasHtml"
// Remeda
toCamelCase("HasHTML"); // "hasHTML";
toCamelCase("HasHTML", { preserveConsecutiveUppercase: true }); // "hasHTML";
toCamelCase("HasHTML", { preserveConsecutiveUppercase: false }); // "hasHtml";
toKebabCase
The remeda implementation doesn’t perform deburring (changing of accented latin characters to simple LATIN-1 characters), and doesn’t remove apostrophes.
toSnakeCase
The remeda implementation doesn’t perform deburring (changing of accented latin characters to simple LATIN-1 characters), and doesn’t remove apostrophes.
Not provided by Remeda.
Use the native JS String.prototype.split
with the regex /\s+/u
instead.
// Lodash
words(str);
// Native
str.split(/\s+/u);
Util
when
- Use Remeda’s [
when
] withisNullish
as the predicate and the fallback value wrapped withconstant
. - For defaulting
NaN
values use the built-inNumber.isNaN
instead. - For both you’d need to construct a type-guard manually.
Nullish
// Lodash
defaultTo(DATA, 10);
// Remeda
when(DATA, isNullish, constant(10));
NaN
// Lodash
defaultTo(DATA, 10);
// Remeda
when(DATA, Number.isNaN, constant(10));
Both
// Lodash
defaultTo(DATA, 10);
// Remeda
when(
DATA,
(x) => x === undefined || x === null || Number.isNaN(x),
constant(10),
);
identity
In Remeda the identity
function is a factory that creates an identity
function; it needs to be called to be used.
// Lodash
map(identity);
// Remeda
map(identity());