Module namespace objects: why are properties writable but not mutable in practice?

A module namespace object has all string properties writable and non-configurable. However, due to the [[DefineOwnProperty]] and [[Set]] exotic behaviors, the properties are in fact not mutable. In what case is the writability useful?

1 Like

If it wasn't writable, you could write code that depends on the value not changing.

However, since exports are "live", the value can change at any time, which means it must be marked as writable.

2 Likes

And indeed one of the essential invariants states if the value of a non-configurable data property can change then that property must be marked writable. (The invariant is written as the contrapositive of that - "If P is described as a non-configurable, non-writable own data property, all future calls to [[GetOwnProperty]] ( P ) must return Property Descriptor whose [[Value]] is SameValue as P's [[Value]] attribute." - but it's equivalent.)

2 Likes

That makes so much sense—thanks @ljharb and @bakkot

In theory const exports when out of TDZ could be shown as non writable, as they'd be guaranteed not to change. This is actually something the Moddable implementation of modules in XS does, which is not technically spec compliant. Been wondering if it's a behavior we should specify.

Doing so would expose whether an export used const, or let/var, and would thus become an observable part of a module’s API - making it a breaking change to switch between them - so i don’t think it would be a good thing to specify and expose.

I thought about what @mhofman said as well, but self-rejected it as a backward-incompatible change of ES itself. However, I don't feel like it's any more of an API stability risk than, say, changing the name or length of an exported function, or whether you are freezing an object, provided that was never part of the API contract.

My view is that anything observable, including the things you mentioned, are part of the API contract (except for function toString because that’d be absurd)

Maybe an alternative would be to implement define property in the module namespace exotic such that setting the property to non-writable would succeed if the binding is a const out of TDZ. That's a slightly less observable change and would allow "freezing" the module namespace.

I wonder if there is a reason why properties of module namespace objects were not made to be non-configurable getters without a setter?

Possibly to avoid allocation the extra getter function, it would also create a hidden communications presuming the function isn't frozen.

Oddly enough the Chrome console shows exports in a module object as accessor properties even though this is not observable in code (getOwnPropertyDescriptor shows exports as data properties)

1 Like