The Import Statement Creates A "Live" View Of Modules In ES6 And TypeScript In Angular 2
When I first started learning ES6 by way of TypeScript in Angular 2, I thought that the "import" statements, at the top of my modules, were just using object destructuring assignments. This mental model actually caused me a lot of confusion as I was looking through the Angular 2 source-code - seeing some things that seemingly made no sense. It wasn't until I read Exploring ES6 by Dr. Axel Rauschmayer that I realized these import statements weren't using destructuring at all. Instead, they were creating "live" read-only views of the exported values in the target modules. Exploring ES6 was an amazing book; and, I have zero reason to doubt Dr. Rauschmayer; but, even I had to see this for myself in order to believe it!
Run this demo in my JavaScript Demos project on GitHub.
NOTE: My demo is in the context of Angular 2; but, this really has nothing to do with Angular 2 - this just happens to be the platform on which I am using TypeScript / ES6.
To explore this concept, I basically took the Counter example right out of the "Exploring ES6" book and placed it into an Angular 2 / TypeScript application so we could look at the in-browser transpilation. First, let's look at the counter module:
// Notice that we are exporting a simple value. The "normal" JavaScript mentality is
// that this value would be exported BY VALUE (ie, as a static copy); however, the
// "import" and "export" features of ES6 don't work like your usual variable references.
// They exported as part of a live, read-only view into the module.
export var counter = 0;
// I increment the exported counter.
export function increment() {
counter++;
}
// CAUTION: This is NOT how I would author a service in Angular 2 - this is just to
// demonstrate the way the import / export work in ES6 (in the context of Angular 2).
When I first saw code like this, I only had an ES5 mental model. So, when I saw a "simple value" (think number, string, boolean, date) being exported, I just assumed it was being exported "by value". After all, that's how every other simple-value assignment works in JavaScript. But, if we look at the in-browser transpilation (of TypeScript down to ES5) we can see that it's not quite that simple:
Here, we can see that the transpiled version is re-exporting the "counter" value, so to speak, every time that it is being updated internally within the counter module. So, while the counter value is being updated "by value" locally, we can see that work is being done to keep that value synchronized with the exports.
Ok, now let's import this module into our root Angular 2 component. Remember, this functionality has nothing to do with Angular 2 specifically; Angular 2 is just my platform for exploring TypeScript.
// Import the core angular services.
import { Component } from "@angular/core";
// Import the values from our counter service. In this case, it's import to understand
// that the "import" statement is NOT A DESTRUCTURING STATEMENT (although it may look
// like one). It is, in fact, a "live query" of the given module (kind of like a NodeList
// in the Document Object Model). As such, we are not importing "counter" BY VALUE; in a
// way, we're actually importing BY REFERNCE (for all intents and purposes).
import { counter } from "./counter-service";
import { increment } from "./counter-service";
@Component({
selector: "my-app",
template:
`
<p>
<a (click)="updateValue()">Update value</a> — {{ value }}
</p>
`
})
export class AppComponent {
public value: number;
// I initialize the component.
constructor() {
// Let's copy the value of the counter into the local view-model.
this.value = counter;
}
// ---
// PUBLIC METHODS.
// ---
// I update the value (based on an updated counter).
public updateValue() : void {
// This will increment the counter value in the counter module; which will, in
// turn, update it in our "live query" of the counter module.
increment();
// While the "counter" value is exported "by reference" (so to speak), our "value"
// property is still copied "by value". As such, after we increment the counter,
// we have to store it back into the local view-model.
this.value = counter;
}
}
Again, when I was first learning ES6 and TypeScript, I had thought that these "import" statements were object destructuring assignments. And, as such, I had assumed that the import of a "simple value" like "counter" would have been copied "by value". But, as I learned from "Exploring ES6", this is not destructuring. This is, in fact, creating a live, read-only view into the imported module. When we look at the transpiled version of this component, things become a little more clear:
As you can see, in the places that I thought I was referencing an unscoped simple value, I was, in fact, referencing a property on an object. And, in that case, that object is the "live view" into the counter module. With both transpiled version in mind, we can now see that updating the counter value within the counter module keeps the exports object up-to-date. Then, within our root component, our imported values are really just property references on that synchronized exports object. This is how calling updateValue() in our root component actually keeps our local value in-step with the counter value:
Sometimes, it's hard to stress how important it is to have a solid mental model of how things work. My confusion about import statements as object destructuring assignments was doing me a huge disservice. Thankfully, I finally sat down and read "Exploring ES6" by Dr. Axel Rauschmayer; and, the world started to make sense again.
Want to use code from this post? Check out the license.
Reader Comments
Based on your code samples I am guessing that the Typescript compiler is configured to "module": "system". If you change that to "module": "commonjs" then the generated JavaScript is simpler and is more predictable. Typescript will convert "export var num = 0" to "exports.num = 0" and "++num" to "++exports.num".
@Ori,
Yes, I believe it is set to "system". But, just to be clear, I think you're saying that a compile target of "system" and "commonjs" both accomplish the *same* thing - just that the transpiled code is easier to understand?
Indeed. You just explained me better than I ...
Thats a really nice trick. Also I'd like to assume, all the counter-service counter values are the same reference globall. Calling increment() in module Foo.js also updates the counter in Bar.js. This is correct?