Compatibility
Published
The non-mutating array methods are the first genuinely new array vocabulary in years, and they arrived after most guidance about obfuscation was written. They are also a good test of a distinction that matters more than it sounds: the difference between a transform that rewrites syntax and one that rewrites names.
What was measured
The sample exercises toSorted, toReversed, with, toSpliced, at, findLast and findLastIndex, plus Object.groupBy and Map.groupBy. For each one it records the result and, where it matters, the original array afterwards, since the entire point of this family is that the original is not modified.
It also pins two properties that are easy to lose in a rewrite: that Object.groupBy returns an object with a null prototype, and that a comparator-driven sort is stable, checked by sorting records on a key that half of them share and reading back the original relative order.
All five configurations produced identical output. The sorted, reversed, replaced and spliced copies were correct, the source arrays were untouched, negative indexing through at worked, the grouped object had a null prototype and the stable sort preserved insertion order within each group.
That is the result you would expect once the distinction in the lede is taken seriously. These are method calls on values, not syntax. There is no new grammar for a target-version rewrite to lower, so the base transforms have nothing to do here.
The ES5 target does not polyfill anything
A related question comes up whenever a build targets older environments, and the answer is easy to state incorrectly. Setting the output target to the default ES5 mode lowers syntax. It does not add library methods.
This was checked rather than assumed. In the ES5 output, toSorted, toReversed, findLast and groupBy all appear as plain method calls, and there are zero assignments to Array.prototype or Object anywhere in the file. The protected output is smaller than the input, not larger, which is what you would expect from a transform that added nothing.
So a protected ES5-target build that calls toSorted will throw TypeError: ... is not a function in any browser that predates the method, exactly as the unprotected build would. Protection did not introduce the problem and it does not solve it. If you need these methods in older environments, that is a job for your bundler's polyfill configuration, which runs before protection.
The general form of this rule is worth carrying: a target-version setting changes what grammar the output uses, which is a different question from what your runtime's standard library contains.
The measured curiosity: arr.with survives renaming, arr.at does not
The member column is where this family gets interesting, and it produced the sharpest small result of this pass. Member renaming was pointed at these method names one at a time, using dot access throughout so the result could not be confused with the documented behaviour of string-keyed access.
Pointing it at at broke the call: TypeError: base._0x1 is not a function. Pointing it at toSorted broke it the same way, and pointing it at groupBy produced TypeError: Object._0x1 is not a function. All three are the ordinary, documented consequence of renaming a name the platform owns: your side moves, the platform's side does not.
Pointing it at with changed nothing at all. The output was byte-for-byte the behaviour of the unprotected file, with the replacement copy correct and the original untouched.
The reason is a detail of how the writer emits property access. with is a reserved word in JavaScript, and the writer always emits an access to a keyword-named property in quoted bracket form -- base.with(1, 9) is written out as base["with"](1, 9). A string is not a rename site, so the call survives. The method whose name is reserved is the one that came through untouched, and the ordinary-looking at is the one that broke.
The same mechanism is a hazard in the other direction
That behaviour is benign here and it is worth being clear that it is not benign everywhere, because the mechanism cuts both ways depending on who owns the declaration.
When the keyword-named property belongs to the platform, as with Array.prototype.with, there is no declaration in your file for renaming to touch. The quoted access is left alone, it still names the real method, and everything works.
When you own the declaration, the two halves come apart. An object literal such as { get: fn } has its declaration renamed to a generated member, while the quoted access api["get"](x) is left as a string -- so the call throws at first use. The contextual keywords make this easier to hit than it sounds, because get, set, of, static, async and await are all ordinary names for properties on caches, stores, routers and option objects.
The practical summary is short. A keyword-named method you call on a built-in is safe under member renaming. A keyword-named property you declare yourself is not, and it is the one case where the usual advice to scope your pattern with a regular expression needs checking rather than assuming.
Sorting, stability and the comparator
toSorted takes the same comparator as sort, and inherits the same footguns. The default comparison is lexicographic, so [10, 9, 1].toSorted() orders as 1, 10, 9 with or without protection. That is not a compatibility question, but it is the single most common thing people notice after a build changes and attribute to the build.
Stability was measured explicitly because a rewrite that reordered anything would show up here first. Sorting four records on a key that pairs them preserved the original relative order within each pair in all five configurations.
The comparator itself is ordinary application code and is renamed like any other function. Its parameters were renamed in every configuration and the results were unchanged, which is the expected result: those names never leave the function.
The one thing to watch is a comparator that reads properties by name off the objects it is sorting. That is a member position, and if your pattern matches those property names on the declaration side but the objects come from a JSON payload where the keys are strings, the comparator will read undefined for every record and produce a stable but meaningless order.
How to check your own build
The cheapest check is a few assertions in the protected build that cover both halves. Assert that a non-mutating method returns the right copy and leaves the source array alone, and assert that the methods exist at all in your oldest supported runtime.
If a method call throws is not a function after protection, the question to ask first is whether the same call throws in the unprotected build on the same runtime. If it does, the method is missing from that environment and the answer is a polyfill in your bundler. If it only throws after protection, a member pattern reached the method name.
For the renaming question specifically, the fastest bisect is to run the same file with member renaming off. If the failure disappears, print your pattern and check it against the platform method names your code calls. The names that break are the ordinary-looking ones; the reserved words come through on their own.
Frequently asked questions
Does obfuscation break toSorted, toReversed and with?
Not in anything measured here. The whole non-mutating family, along with at, findLast, findLastIndex, Object.groupBy and Map.groupBy, produced identical output in five configurations, including the fact that the source array is not modified and that grouped objects have a null prototype.
Does the ES5 target polyfill the new array methods?
No, and this was checked in the emitted output rather than assumed. The methods appear as plain calls with no assignments to Array.prototype anywhere in the file. A target-version setting lowers syntax; it does not add library methods. Polyfilling belongs in your bundler, before the protection step.
Why does arr.at stop working after protection when arr.with is fine?
Because with is a reserved word. The writer always emits an access to a keyword-named property in quoted bracket form, and a string is not a rename site, so that call survives member renaming. The ordinary name at is rewritten on your side while the platform keeps the real method, so the call fails.
Is it safe to rename members when my code uses these methods?
It is safe if your pattern matches names your own objects declare rather than method names on built-ins. Every failure measured here came from a pattern that matched a platform method. Scoping the pattern to your own vocabulary is the mitigation, and it is worth verifying rather than assuming.
Does member renaming affect sorting stability?
No. Stability is a property of the sort implementation, not of the names in your file, and a comparator-driven sort preserved insertion order within equal keys in all five configurations. A comparator that reads properties by name can still be affected, because those reads are member positions.
Why does my protected build sort numbers in the wrong order?
Almost certainly because the call has no comparator. The default comparison converts elements to strings and orders lexicographically, so 10 sorts before 9, before and after protection. Pass an explicit comparator and the order is the same in both builds.
Related reading