Migrating to v2
Migrating to v2
v2 is the first major version release of Remeda in almost 2 years. We took this
opportunity to gather as many breaking changes as possible into a single
release, focusing on modernization and simplification. Importantly, this release
doesn’t change any major aspect of Remeda’s architecture. Almost half of
Remeda’s exported functions don’t have any breaking changes, neither in their
runtime nor their typing!
Migrating
For most projects, only minor changes to how some functions are called will be
necessary.
Some functions have parameter types or return types that have changed;
these may require adjustments either upstream or downstream from where the
function is called. These changes need more attention as they might expose
existing bugs in your codebase (similar to better typing or a new lint rule).
A few functions have breaking changes in their runtime implementation, mainly
in how edge-cases are handled. These also require careful attention during
migration to ensure the new implementation behaves as expected.
To facilitate this process, we provide a function-by-function migration
guide that details the changes for each function, includes examples of potential
breakages, and offers solutions, including ways to maintain previous behaviors.
We recommend first updating to the latest v1 version, fixing any deprecation
errors, and then updating to the latest version of Remeda, using this guide to
address any remaining issues.
The following chapters provide an overview of the changes, offering a broader
perspective and motivation for each change. All relevant information for each
function is repeated in each function’s migration documentation.
Environment
Remeda v2 is built for modern environments. Browsers and runtimes that don’t
support the minimum requirements might still be able to use some functions (if
their implementation doesn’t rely on anything more modern), but those cases will
not be supported.
Runtime ≥ ES2022
Previously, Remeda compiled down to a target of ES5 (and ES2017 lib).
This meant that modern JavaScript features (like object and array spreading) had
to be polyfilled and shipped with each function that used them. It also meant
that we couldn’t use certain features, like built-in iterators (e.g.,
Array.prototype.entries
) or bigint
s.
v2 is compiled with a target of ES2022 (and ES2022 lib), which is
supported by all currently maintained Node.js versions (18+) and by ~93.8%
of all browsers.
TypeScript ≥ 5.1
The minimum TypeScript version our exported types are tested against is 5.1,
up from 4.2 in v1.
We currently don’t use any new language features that were only added in
recent versions of TypeScript, but we might want to use them in the future
without requiring a breaking change.
Importing
Remeda v2 builds its packaged files using tsup
(replacing the bare tsc
build of the previous version), with full support for
tree-shaking, code splitting, and minification. The output config is validated
using both attw
and publint
.
This results in completely different output artifacts and structure for both
CommonJS and ESM. We don’t expect this to have any impact on your
project; it should integrate cleanly with any modern JS build tool, bundler, and
runtime.
Removed Variants
In v1, Remeda offered several “variants” of the base runtime implementation and
typing via properties added to the exported function. These have been removed in
v2, and their usage merged into the base functions.
Indexed
The indexed
variant allowed callback functions (predicates, mappers, etc.) to
use 2 additional parameters in their signature: the index
, representing the
offset of the item within the data array, and the data
array itself. These
were provided to most functions but weren’t offered consistently. The
implementation added runtime checks on every invocation, even when the indexed
variant wasn’t used. In v2, the indexed
“variant” of the callback is now
available on the base implementation and has been added to all functions. This
aligns with the signatures of the built-in functions of Array.prototype
.
const DATA = [1, 2, 3] as const;
// Was
map.indexed(DATA, (item, index) => item + index);
// Now
map(DATA, (item, index) => item + index);
Object-based functions (like mapKeys
) also got the same treatment, where the
callbacks are called with the prop’s key
as the 2nd parameter (instead of the
numerical index
for arrays).
Migration
For calls that used the indexed variant, simply remove the .indexed
suffix.
For the rest, you most likely don’t need to do anything.
Note: If the callback function was passed by reference and not via an
inline function, your callback function would now be called with additional
parameters. If the function signature no longer matches, TypeScript would
complain about the type mismatch. In more complex cases, if the function
signature does match, it will now be called with additional parameters and
might compute results differently. This is rare and can only happen if the
callback function already accepted an optional number
or
number | undefined
as its second parameter. To fix this, simply wrap the
callback with an inline function that takes a single parameter. ESLint’s Unicorn
plugin’s unicorn/no-array-callback-reference
is recommended to detect potential cases of this issue.
const DATA = ["1", "2", "3"] as const;
// BUG! `parseInt` takes an optional 2nd `number` param for the radix!
map(DATA, Number.parseInt); //=> [1, NaN, NaN]
// Fix:
map(DATA, (item) => Number.parseInt(item)); //=> [1, 2, 3]
Strict
We sometimes come up with improved typing for a function’s return value. The
type is often more complex and makes more assumptions about the inputs, making
it incompatible with the existing type. In these cases, we created a strict
variant with the same runtime implementation but with improved typing. In v2,
all strict variants are now the default, removing the original base typing.
This change can result in downstream assumptions about types breaking or
becoming invalid. In most cases, we believe these are valid typing issues being
surfaced for the first time because of the improved typing.
const DATA = ["1", "2", "3"] as const;
const was = map(DATA, (item) => Number.parseInt(item));
// ^? number[];
const now = map(DATA, (item) => Number.parseInt(item));
// ^? [number, number, number]
Migration
For calls that used the strict variant, simply remove the .strict
suffix. For
the rest, you most likely don’t need to do anything.
If you encounter new TypeScript issues following this change, we recommend first
checking if this issue is the result of the better typing. Note that if you use
inferred typing a lot, the issue might only surface further downstream and not
at the place the function is called.
To bypass or work around these issues:
- The function-specific migration guides below also suggest possible type
assertions that could be used to get the “legacy” types back.
- Simplify the types by using the TypeScript
satisfies
keyword instead of
as const
.
- You can use explicit, less specific types in the generics of the functions to
force them to a specific type instead of the inferred type.
- Most of the new types should be extendable by the old types, meaning you can
cast the output to the type you expect to simplify the result.
- Some new types might be hard to read and understand via the IDE’s tooltips. In
those cases, you can use Type-Fest’s
Simplify
to debug the resulting type (in most cases, we already wrap the types with Simplify).
Important: The types might have edge cases that we didn’t foresee and test
against. If you feel that the computed type is wrong, please report it on GitHub.
// Downstream bugs revealed:
// @ts-expect-error [ts(2493)]Tuple type '[number]' of length '1' has no element at index '1'.
const [, buggy] = map(["1"] as const, (x) => Number.parseInt(x));
// Get the legacy behavior:
const strict = map(["1", "2", "3"] as const, (x) => Number.parseInt(x));
// ^? [number, number, number]
const satisfied = map(["1", "2", "3"] satisfies `${number}`[], (x) =>
// ^? number[];
Number.parseInt(x),
);
const generalized = map<`${number}`[], number[]>(
["1", "2", "3"] as const,
(x) =>
// ^? number[]
Number.parseInt(x),
);
const casted = map(["1", "2", "3"] as `${number}`[], (x) =>
// ^? number[];
Number.parseInt(x),
);
const castedOutput = map(["1", "2", "3"] as const, (x) =>
Number.parseInt(x),
) as number[];
Lazy (Internal)
The lazy
variant wasn’t documented but still existed on many functions.
Unlike the previous variants, it wasn’t another implementation of the function,
but a tool used internally by the purry
and pipe
functions to allow lazy
evaluation of functions. This abstraction has been completely removed.
Migration
If you exported a lazy
property from your internal functions to make them
lazy within Remeda’s pipe
, use purry
with the lazy implementation as the 3rd
parameter instead.
We consider this API internal and thus don’t provide documentation or export the
types and utilities that would make it easier to work with (the ones we use
internally). If you need these APIs, please open an issue on GitHub.
Headless Invocation
A few single-parameter functions in v1 did not offer a properly curried
“dataLast” implementation and instead suggested using a “headless” version for
“dataLast” cases (e.g., keys
). This created problems with more advanced types
not being inferred correctly, requiring a properly curried version instead
(e.g., first
). We felt that this case-by-case difference made your code more
error-prone and confusing. In v2, all single-parameter functions should now be
called with no parameters to get their dataLast implementation.
The only headless functions remaining are type-guards (e.g., isString
,
isDefined
).
// Was
pipe(DATA, keys);
map(DATA, identity);
filter(DATA, isString);
// Now
pipe(DATA, keys());
map(DATA, identity());
filter(DATA, isString); // Not changed!
Migration
Most call sites should now show an error when using the headless function
because TypeScript wouldn’t be able to infer the type correctly. However,
because there is no way to deprecate the “headless” nature of a function (it’s
just a function-object), you will have to manually search for them. The
functions are: clone
, identity
, fromPairs
*, keys
, randomString
,
toPairs
*, and values
.
* These functions have been renamed and their renamed versions already don’t
support headless invocation in v1.
Renamed and Removed
Removed
To offer the best possible functions, we deemed several functions as redundant
when they could be easily replaced with other existing functions, resulting in
code of the same length. In all these cases, the replacement is a composite of
at most three functions.
// Was
compact(DATA);
// Now
filter(DATA, isTruthy);
Other functions were removed because their logic was either split into several
other functions or merged into a more general-purpose tool to allow better code
reuse and improved typing.
// Was
flatten(DATA);
flattenDeep(DATA);
// Now
flat(DATA);
flat(DATA, 10);
The functions are: compact
, countBy
, flatMapToObj
, flatten
,
flattenDeep
, isObject
, maxBy
, minBy
, noop
, reject
, type
, and
zipObj
.
Renamed
Remeda took a lot of its early inspiration from Lodash and Ramda. Many functions
were named similarly to their equivalents in those libraries, but these names
don’t always align with the names chosen by the ECMAScript standard. We chose to
prefer the standard names.
// Was
pipe(DATA, toPairs(), ..., fromPairs());
// Now
pipe(DATA, entries(), ..., fromEntries())
We also decided to improve some names by dropping abbreviations and partial
spellings in favor of proper English words.
// Was
uniq(DATA);
// Now
unique(DATA);
The functions are: createPipe
, equals
, fromPairs
, isNil
, toPairs
,
uniq
, uniqBy
, and uniqWith
.
Migration
The latest versions of Remeda v1 have all renamed and removed functions
deprecated with suggestions for how to migrate. Doing this while still in v1
would make it easier to replace them one-by-one. Otherwise, this document has a
deprecated section with migration instructions too.
Object Keys
Most of the functions that provided a way to traverse object
s relied on the
built-in Object.entries
.
This function has limitations on which properties it iterates upon (enumerates):
number
keys are cast as string
.
symbol
keys are ignored.
To properly reflect this, we had to change the typing for both the callback
functions and the return types. Functions that returned an object would either
drop the symbol keys (if constructing a new object) or copy them as-is (if
cloning the original object).
It’s important to note that only the types have changed here; the runtime
behavior remains the same. number
keys are always cast as strings in
JavaScript; myObj[0]
and myObj["0"]
access the same property. This change
will not construct your objects differently than they used to be. To provide
more utility, the implementations of omit
and omitBy
have been changed
to preserve symbol keys.
Read more about this on MDN.
Migration
The biggest differences are due to the change in how we handle symbol
keys.
symbol
usage is rare, and if you don’t know you use it in your project, you
most likely don’t.
number
keys require a little more attention, especially if you are checking or
using the keys by value (and not just passing them around). Because only types
have changed (and not the runtime behavior), you might run into new TypeScript
(or ESLint) warnings and errors due to surfacing previously existing issues.
The affected functions are: entries
, evolve
, forEachObj
, keys
,
mapKeys
, mapValues
, omit
, omitBy
, and pickBy
.
Re-Implementations
Several functions had their runtime implementation changed, including changes to
their semantics, so that they’d return different results in v2 for some edge
cases. These changes are documented below for each function. The functions are:
clone
, difference
, intersection
, omit
, omitBy
, purry
, sample
,
and zipWith
.