16

globals.ts:

export let TimeZone: number = 0;

app.js

import * as globals from "./globals";
globals.TimeZone = -5;

The last line gives:

error TS2540: Cannot assign to 'TimeZone' because it is a read-only property.

Why?

Old Geezer
  • 14,854
  • 31
  • 111
  • 198
  • From https://github.com/Microsoft/TypeScript/issues/6751: *"That is how ES6 modules work - imports are constant."* – UnholySheep Dec 04 '18 at 17:00
  • Also https://stackoverflow.com/questions/38060519/es6-import-as-a-read-only-view-understanding and http://exploringjs.com/es6/ch_modules.html#sec_imports-as-views-on-exports – artem Dec 04 '18 at 17:02

1 Answers1

24

Imports are a read-only view of the exported binding in the source module's environment. Even if the source binding is mutable (as in your example), you can't use the imported view of it to modify it. Only the module exporting it can modify it.

Why? Because one module importing the variable shouldn't be able to reach into the source module and change the variable's value. If the source module wants to make it possible for modules using it to change the value of the exported variable, it can expose a function to do that. (Or expose an object with mutable properties.)

Remember that modules are shared across the various modules importing them. So if Modules A and B both import Module C, you don't want Module A modifying what Module B sees (even if Module C can, because it's a variable, not a constant).¹

FWIW, here's an example (live copy on plunker):

index.html:

<!DOCTYPE html>
<html>

  <head>
  </head>

  <body>
    <p id="display">0</p>
    <script type="module" src="imp1.js"></script>
    <script type="module" src="imp2.js"></script>
  </body>

</html>

counter.js:

export let counter = 0;

export function increment() {
  ++counter;
}

imp1.js:

import { counter } from "./counter.js";

const display = document.getElementById("display");

setInterval(() => {
  // This module sees changes counter.js makes to the counter
  display.textContent = counter;
}, 100);

imp2.js:

import { counter, increment } from "./counter.js";

// Trying to set it directly fails
try {
  counter = 127;
} catch (e) {
  document.body.insertAdjacentHTML(
    "beforeend",
    "imp2: Failed to directly set counter to 127"
  );
}

setInterval(() => {
  // But it can ask counter.js to do it
  increment();
}, 200);

I should note that although you can't modify the binding (TimeZone in your example), if you export an object, that object's state can be modified. For example:

// In the exporting module
export const container = {
    TimeZone: 0,
};

// In the importing module
import { container } from "./module.js";
container.TimeZone = 42;

That works, and the change is seen by any code that also imports container. Sometimes you want to defend against that, which you might do with Object.freeze (if the source module doesn't need to ever change the properties of the object) or a Proxy.

But again, changing an object's state is not the same as changing the value of the binding that was exported.


¹ "...if Modules A and B both import Module C, you don't want Module A modifying what Module B sees..."

Well, okay, sometimes you do want to do that. :-) When you do, export an object instead:

export const sharedObject = {/*...*/}; // Or a `Map`, or `Set`, or whatever

...and have the modules modify its properties. All modules import the same object, so modifications to its properties are seen by all of them.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • commonjs let us – java-addict301 Oct 25 '21 at 16:41
  • @java-addict301 - I don't think so, can you show me an example? You might be mistaking modifying an object's state (which you can do in both CommonJS and ESM) with modifying an exported variable (which I don't think you can do with either). (It's a lot like function parameters: In a function, you can assign a new value to a param, but that has no effect on the argument that was passed in; but you can change the *state* of an object that's passed in, and that change is also visible at the callsite.) There are differences in CommonJS and ESM, of course, but I don't think this is one of them...? – T.J. Crowder Oct 25 '21 at 17:09
  • "if Modules A and B both import Module C, you don't want Module A modifying what Module B sees" - sure I do, sometimes; thankfully I can "export an object" for that. – sdbbs Jul 18 '23 at 01:02