Compatibility
Published
Metaprogramming code names a property twice: once in the syntax that reads it, and once in a string handed to an API. A member transform rewrites the first and leaves the second alone. That is the entire story, and once you can see the line it draws you can predict which of these APIs are affected without running anything.
Renaming happens in three places, and a bracket is not one of them
The member transform walks your syntax tree and rewrites a name in exactly three situations. The first is the key of an object literal written as an identifier, as in { count: 1 }. The second is the key of an object literal written as a quoted string, as in { "count": 1 }. The third is a dotted member access, as in obj.count.
The second one surprises people, so it is worth stating plainly: quoting a key in an object literal does not opt it out. A quoted key is still a key in a literal, the transform can see it is a key, and it rewrites it along with the rest.
What the transform never rewrites is the argument of a computed access. obj["count"] reaches the property through an expression that happens to be a string literal, and the transform treats that string as a value rather than as a name. This is not an oversight. A computed key can be any expression at all, so in the general case there is nothing for a static transform to decide, and a rule that rewrote string literals in bracket position would break every object whose keys come from data.
So the line falls here. Syntax that names a property gets rewritten. A string that names a property does not. Every case below is a consequence of those two sentences.
A Proxy trap receives the name the code actually used
When you wrap an object in a Proxy, your get trap is handed the key the access site used. If your code reads obj.count and the transform rewrote that site to a short name, the trap is handed the short name. Not count. Whatever the transform picked.
Meanwhile the literal inside your trap, the one in if (prop === "count"), is a string. It sits in an ordinary comparison, it is not a key in a literal and it is not a dotted access, so nothing renames it. One side of the comparison moved and the other did not.
The result is the failure mode that makes this whole family of bugs expensive: the branch simply stops being taken. Nothing throws. The trap runs, the comparison is false, the default path returns, and the behaviour you attached to that property quietly disappears. If the trap was enforcing something, it stops enforcing it. If it was logging, it stops logging.
The useful corollary is that a trap which does not name anything is completely safe. A pass-through trap that forwards whatever key it is given, of the form get(t, prop, r) { return Reflect.get(t, prop, r); }, never mentions one of your property names, so there is nothing in it that could disagree with the renamed access sites. Traps that intercept generically are fine. Traps that hardcode a list of your own property names are the risk.
Reflect, defineProperty and descriptors are all the same shape
Once you know the rule, this group needs no separate analysis. Reflect.get(o, "count"), Reflect.has(o, "count"), Object.defineProperty(o, "count", d), Object.getOwnPropertyDescriptor(o, "count") and "count" in o all name the property with a string argument. None of them are rewritten.
The failure is one-directional, and that is what makes it easy to reason about. If you define a property with a string and read it with a dot, the read is renamed, the definition is not, and the read returns undefined. If you define with a dot and read with a dot, both sites are rewritten consistently and everything works. If you define with a string and read with a string, neither is rewritten and everything works.
So the rule for mixed code is to be consistent in form, not merely consistent in spelling. Pick dotted access or computed access for a given property and stay with it, or take the property out of the transform's reach entirely by reserving the name.
One related behaviour catches people out in the other direction. Object.keys, for...in and destructuring by name all report the new names, because they read the object as it actually is at runtime. Code that enumerates its own object and compares the results against a hardcoded list of expected keys will find none of them.
The safe case: WeakMap, WeakSet and anything keyed on identity
There is a whole family of APIs here that is structurally unaffected, and it is worth knowing which one it is because it gives you a migration target.
A WeakMap keys on object identity. There is no property name anywhere in the operation: you pass an object as the key, the map holds a weak reference to it, and nothing about the arrangement involves a name that a transform could rewrite. The same is true of WeakSet, and of WeakRef and FinalizationRegistry.
This is a real practical advantage of the WeakMap private-data pattern over the older convention of an underscore-prefixed property. The convention stores private state as a property called _internal, which is a member name and is renamed like every other member name. The WeakMap version stores it in a map keyed by the instance, which the transform has no reason to touch. Both express the same intent; only one of them is invisible to member renaming.
Symbol-keyed properties sit in between and are usually fine. A symbol is a value held in a binding, and that binding is renamed like any other binding, but it is renamed at every site consistently, so the property continues to resolve. The exception is the one you would expect: a symbol obtained from a registry by string, via Symbol.for("count"), carries a string that is not renamed, and it will not match a property that was defined through a renamed path.
Framework reactivity is usually somebody else's trap
Vue's reactivity system and MobX both wrap your objects in proxies, which prompts a reasonable question about whether protecting your application breaks them. Usually it does not, and the reason follows directly from the pass-through rule.
The framework's trap is generic. It intercepts every property, forwards the key it was given, and tracks dependencies by that key. It does not contain a list of your property names, because it has never seen your application. So it observes whatever names your protected code uses, consistently, and the dependency graph it builds is correct in the new vocabulary.
Where this does bite is at the edges the transform cannot see, which is the same rule that governs every member-renaming decision. A template that references a property by name is a separate artifact; if it is not part of the same transform, it keeps the original name and looks up a property that no longer exists. Anything outside your protected files that refers to a member by name has to be reserved, and the member documentation lists the categories worth walking before you enable the option.
What to do about it
Start by enumerating, because this is one of the rare compatibility questions with a complete and finite answer. Search your code for every place a property is named as a string rather than as syntax: the second argument of defineProperty, the key argument of a Reflect call, comparisons inside proxy traps, Symbol.for registrations, and any list of expected keys. That set is your reserved list.
Add those names to the reserved names configuration so the transform leaves them alone at every site. Reserving is better than rewriting your code to be form-consistent, because it survives someone later adding a dotted access to the same property without knowing the constraint.
Prefer generic traps to name-matching traps where you have the choice. A trap that switches on a hardcoded property name is a dependency on that name surviving the build; a trap that forwards is not.
And if you cannot enumerate the surface, running with the member option off is a legitimate configuration rather than a defeat. The documentation makes that recommendation directly, and the rest of the pipeline is unaffected by the choice.
Whatever you choose, rely on behaviour to detect the problem rather than on the build. There is no error to catch here. The object still exists, the property no longer does, and the only thing that notices is a test that asserts what the code was supposed to do.
The short version
Syntax that names a property is rewritten. A string that names a property is not. A proxy trap sees the renamed key while its own literals stay put, which is why name-matching traps stop matching; pass-through traps are unaffected. Reflect, defineProperty and descriptor calls all take the property name as a string, so they keep the original and desynchronise from any dotted read of the same property. Identity-keyed collections such as WeakMap involve no names at all and are untouched. Reserve every name you pass as a string, and test for behaviour, because none of this produces a build error.
Frequently asked questions
Does member renaming rewrite quoted object-literal keys?
Yes. Both the identifier form and the quoted form are keys in an object literal, and the transform rewrites both, so quoting a key does not opt it out. What is left alone is a computed access such as obj["count"], where the string is an argument to an expression rather than a key in a literal.
Why does my Proxy get trap stop matching property names?
Because the trap receives the key that the access site actually used, and the access site was renamed while the string literal inside your trap was not. If your code reads obj.count and the transform rewrote that to a short name, the trap is handed the short name and a comparison against "count" no longer matches. Nothing throws; the branch simply stops being taken.
Is Object.defineProperty affected by obfuscation?
The property name you pass to it is a string argument, so it is not renamed. That becomes a problem when the matching read is dotted, because the read is renamed and the definition is not, so the read returns undefined. Define and read through the same form, or reserve the name so neither site changes.
Do WeakMap and WeakSet survive member renaming?
They are unaffected, because they key on object identity rather than on a property name, so there is nothing for the transform to rewrite. That is one reason the WeakMap private-data pattern holds up better under protection than the underscore-prefixed-property convention it replaced.
Does Vue or MobX reactivity break under member renaming?
The framework's own trap forwards whatever key it is given rather than hardcoding your property names, so it stays consistent with your renamed code. The risk sits in templates and any other artifact outside the protected files that names a property, because those are not part of the transform and keep the original name.
How do I find these problems before shipping?
Assume you will not get a build error, because you will not. List every place a property is named as a string rather than as syntax, add those names to the reserved list, and rely on behavioural tests rather than on the build. If you cannot enumerate them, running with the member option off is a legitimate configuration.
Related reading