Compatibility
Published
JavaScript gives you three different ways for a value to be missing, and a surprising amount of application logic turns on telling them apart. A property that was never set is not the same as one explicitly set to null, and neither is the same as one set to zero or an empty string. Any transform that rewrites your code has to preserve those distinctions exactly, so we measured whether ours does.
What was measured
The sample classifies values into undefined, null and their type, reads present, empty, null and absent properties through a helper that falls back only on undefined, compares a null property with both loose and strict equality, distinguishes a property that exists and holds null from one that does not exist using the in operator, checks void 0 against undefined, declares a local variable named undefined to test shadowing, applies typeof to an identifier that is not declared anywhere, and reads the hole out of a sparse array.
It was run unprotected in node, then protected in five configurations and run again, diffed line by line: the ES5 target on defaults, the modern target, the two identifier-renaming presets our emit-validation gate uses, and a string-table preset that moves and encodes every literal.
All five produced output identical to the original. Every distinction held. A property holding zero was still distinguished from an absent one, an explicit null was still reported as null rather than as a fallback, void 0 === undefined was still true, the loose comparison against null was still true while the strict one was still false, in still separated a null-valued property from an absent one, typeof on an undeclared identifier still returned the string undefined instead of throwing, and the array hole still read as undefined with the length preserved.
This is a clean result across the board, and for the default configuration most readers use it is the whole answer.
A local named undefined, and why renaming it is a fix rather than a risk
One arm of the sample looks like it should be a problem. It declares a function-scoped variable actually named undefined, holding the string shadow, then reads that name three ways from inside the function.
That is legal JavaScript and it is a genuine hazard in hand-written code, because every undefined inside that function now refers to a local string rather than to the global value. In the unprotected run, the comparison void 0 === undefined is therefore false and typeof undefined is string.
Under the renaming presets the declaration became an ordinary generated identifier holding the same string, and every read inside the function was rewritten with it. The output was identical, which is the correct result: renaming is a consistent substitution, so the shadowing relationship is preserved exactly even though the confusing name is gone.
It is a small illustration of a general point. The transform does not need to understand what undefined means to handle it correctly, because it is rewriting a binding and all of that binding's uses together.
The one thing that moves: optional reads written with a string key
The member-renaming column found a single failure, and it is the accident this whole family of code is prone to.
The sample's optional-read helper takes an object and a key and returns a fallback when the value is undefined. Every call site passes the key as a string literal. Point member renaming at those property names and the declarations on the object are renamed while the string literals passed to the helper are not, because a string is a value and not a member access site.
The measured result is that every optional read returns the fallback. The property holding zero, the empty string, the explicit null and the genuinely absent one all come back identical, which is precisely the distinction the code existed to make. The in check on a property that still exists under a different name also flipped to false.
No error is raised. A function whose entire job is to tell missing from present now reports everything as missing, and it does so consistently enough to look deliberate. Renaming a property name that never existed in the first place, by contrast, changed nothing at all, which is a useful control: the failure comes from the mismatch, not from the renaming.
Reading the result correctly
It would be easy to take the last section as a reason to avoid member renaming. That is not what the measurement says.
Renaming works by rewriting declarations and dot accesses together. Wherever both halves are visible to it, the substitution is consistent and behaviour is preserved, which is why every other arm of this sample came back identical. The failure appears only where a name has a second reader the transform cannot rewrite, and here that second reader is a string literal in your own source.
So the fix is not to turn the option off but to stop addressing renamed properties by string. Where you can, read them with dot access so both sides move together. Where you genuinely need dynamic keys, keep those property names outside the renaming pattern. This is the same conclusion the site reaches for schema fields, serialised payloads and required-field lists, and it is the same rule with the same boundary.
How to check your own build
The audit is a grep. List the property names your renaming pattern will match, then search your source for each one in quotes. Every hit is a place where the transform will rewrite one side of a pair and not the other.
For the behaviour itself, a short assertion suite is enough and does not need our engine to run it: build one object carrying zero, an empty string, false, an explicit null and an absent property, then assert that your optional-read helper returns the stored value for the first four and the fallback only for the last. Run it before and after protection and compare. If those five cases agree, this area is settled for your codebase.
It is worth keeping the distinction visible in the code as well. A helper that falls back on undefined specifically, rather than on any falsy value, is both more correct and easier to reason about after a transform, because its contract is narrow enough to test in one line.
The short version
The three ways a value can be missing survive protection intact. Absent, undefined and null stayed distinguishable in all five configurations, along with void 0, loose against strict equality, the in operator, an array hole, typeof on an undeclared identifier, and even a local variable that shadows undefined.
The single measured failure is an optional read that addresses its property with a string key while member renaming rewrites the declaration. It returns the fallback for everything and raises nothing. Read renamed properties with dot access, keep string-addressed names out of the pattern, and the distinction your logic depends on stays intact.
Frequently asked questions
Does obfuscation change how undefined behaves?
No. Across five configurations, including both language targets, identifier renaming and string encoding, every distinction we measured held: absent against undefined against null, void 0 against undefined, loose against strict equality, the in operator, and an array hole. The output was identical to the unprotected run in each case.
Is void 0 treated the same as undefined after protection?
Yes. The comparison void 0 === undefined returned true in every configuration, and the classification helper reported void 0 as undefined exactly as it did before protection. Note that the engine does not rewrite your ordinary undefined references into void 0; they are preserved as written.
Does an explicit null still survive as null?
Yes. A property explicitly set to null was reported as null rather than replaced by a default in all five configurations, and the loose and strict comparisons against undefined still returned true and false respectively. That distinction is intact on the default configuration.
What happens to a local variable named undefined?
It is renamed to an ordinary generated identifier along with all of its uses, and the behaviour is preserved exactly. Because renaming is a consistent substitution, the shadowing relationship the original code created still holds, and our sample produced identical output before and after.
Why does my optional read return the fallback for everything?
Because member renaming rewrote the property declarations while the string keys your reader passes were left as written. We measured exactly this: values of zero, empty string, null and a genuinely absent property all came back as the fallback, with nothing thrown. Read the properties with dot access, or keep those names out of the renaming pattern.
Does typeof on an undeclared identifier still avoid throwing?
Yes. It returned the string undefined in every configuration, which is the behaviour the language specifies and the reason feature-detection code is written that way. Identifier renaming only rewrites bindings your file declares, so a name it never declares is left alone.
Related reading