Status: accepted; the destructuring half was reversed after use (see below)
Props must be real JavaScript values passed by reference (no serialisation, no copies, identical object identity), must be able to update reactively without re-initialising the child, must be readonly from the child’s perspective, and must not silently become stale when destructured.
props.user === user for the object the parent passed.JSON.stringify, no structured clone, no defensive copy.Object.freeze on user objects (it is an observable mutation of someone
else’s value).Proxy if it costs identity or measurable speed.Proxy over a reactive record. Uniform, supports in/ownKeys, but adds
a trap on every property access in the hottest path of component code, and
proxies are not free in any engine. Identity of the props object itself
becomes a proxy, which leaks into === comparisons and devtools.Option 4.
<UserCard user={currentUser.value} label="hi" />
emits
createComponent(UserCard, {
get user() {
return currentUser.value;
},
label: 'hi',
});
No setters are defined, so the props object has no writable top-level properties. The values themselves are the parent’s own references.
Immutability is enforced by ReadonlyProps<T> (deep-readonly mapped type), by
the absence of setters, and by an ESLint rule that flags mutation through
props. The README states explicitly that runtime deep immutability is not
enforced and why.
Destructuring is rewritten into live reads:
const Row = component<RowProps>(({ todo, count = 0, ...rest }) => (
<li {...rest}>
{todo.title}: {count}
</li>
));
compiles to
const Row = component<RowProps>((props) => {
const rest = rest_(props, ['todo', 'count']);
return (
<li {...rest}>
{props.todo.title}: {props.count ?? 0}
</li>
);
});
Every reference becomes a property read on the props object. A default is
re-applied on every read, which is what the language means by a default. A rest
element becomes one object of getters that delegate back to the props, so
forwarding {...rest} forwards live values rather than a snapshot.
The rewrite is a rename, not a wrapper: no closure, no proxy, nothing allocated per read, and the emitted code is what a developer would have written by hand.
What cannot be rewritten soundly is still an error naming the reason: assigning to a destructured prop (props are readonly), a computed key (it would have to be resolved at setup time, which is the snapshot this avoids), an array pattern (props are an object), and any pattern the rewrite cannot follow.
A default is re-applied on every read, which is what the language does, and it
applies to undefined alone — also what the language does. Amended (0.12.0):
the emitted form was props.count ?? 0, and ?? answers for null as well, so
a parent passing null got the default back instead of the null it asked for.
null is what an API returns for “known to be empty”, which is a different
answer from “not given”, and a component could not tell them apart. It is now
props.count === undefined ? 0 : props.count. Two reads of an accessor rather
than one, which is the price of meaning the same thing the language means —
and the compiler test asserted the wrong output verbatim, so the behaviour was
blessed rather than merely untested.
The first version of this ADR rejected destructuring outright, on the argument that a rewrite which “silently stops applying at the edge of what it can prove” is worse than an error. That argument was wrong about where the edge is. The patterns people actually write — plain keys, renames, defaults, nesting, rest — are all rewritable, and the ones that are not are recognisable syntactically rather than by analysis, so the error is exact rather than approximate.
The original reasoning also ignored the cost it was imposing: ({ todo }) =>
is the shape people reach for, and refusing it made every component read worse
in order to protect against a case the compiler can see coming.
One props object per component instance, with one accessor descriptor per dynamic prop, created once. Nothing is allocated when a prop value changes.
props.user always reads current, and so does user from
({ user }) => ... — the compiler makes the second mean the first.props itself around is fine; its identity is stable.Proxies (access cost and identity leakage), copies or freezing (violates identity / mutates foreign objects), re-initialising the child (React model, excluded), and silently tolerating destructuring (the exact stale-value class of bug this framework exists to remove).