TypeScript modules and cyclic dependencies

Topics: Language Specification
Nov 5, 2013 at 7:33 PM
I've read that TypeScript modules are supposed to be as close as possible to ES6 modules, but they seem fundamentally more limited. According to http://wiki.ecmascript.org/doku.php?id=harmony:modules_examples#cyclic_dependencies ES6 modules support cyclic dependencies, but this doesn't work at all in TypeScript. Lack of cyclic dependency support prevents TypeScript modules from being used as a namespacing mechanism. The following TypeScript code is equivalent to the example from the ES6 wiki:
module Even {
    import odd = Odd.odd;
    export function even(n) {
        return n == 0 || odd(n - 1);
    }
}
module Odd {
    import even = Even.even;
    export function odd(n) {
        return n != 0 && even(n - 1);
    }
}
However, it generates JavaScript that will crash at runtime:
var Even;
(function (Even) {
    var odd = Odd.odd;
    function even(n) {
        return n == 0 || odd(n - 1);
    }
    Even.even = even;
})(Even || (Even = {}));
var Odd;
(function (Odd) {
    var even = Even.even;
    function odd(n) {
        return n != 0 && even(n - 1);
    }
    Odd.odd = odd;
})(Odd || (Odd = {}));
The output I expected from the compiler was something more along the lines of this:
var Even;
(function (Even) {
    function even(n) {
        return n == 0 || Odd.odd(n - 1);
    }
    Even.even = even;
})(Even || (Even = {}));
var Odd;
(function (Odd) {
    function odd(n) {
        return n != 0 && Even.even(n - 1);
    }
    Odd.odd = odd;
})(Odd || (Odd = {}));
Import aliases have been inlined, which implements the correct ES6 semantics. Was this an oversight or a deliberate design decision? Are there any plans to support full ES6 module semantics?
Coordinator
Nov 6, 2013 at 1:15 AM
This was a trade-off; when compiling to pre-ES6 it isn't always going to be possible to fully emulate all ES6 features. We recognized the problem with forward-referencing, but there were other concerns around performance, correctness, and codegen size.

From a performance perspective, subbing in the full dotted form is going to cause extra object lookups every time the imported alias is used, which might not be obvious. Obviously this isn't going to be a big problem in every case, but in tight loops or where the alias name is very 'deep', you could incur nontrivial overhead here.

We also can't guarantee correctness. For example:
module M {
    export class C { /* ... */ }
}

module A {
    import C = M.C;
    module Q {
        var M = 4;
        var x = new C(); // OK if we emit var, but wrong if we emit M.C
    }
}
In this case we could detect it and generate an error, but that defeats one of the more important use cases of importing.

Finally there's a modest increase in file size (usually) if we inline the name. Generating the smallest possible code is not a primary concern, but it's one more point in favor of using 'var'.

Since you can work around the problems with the 'var' approach by not using imports, but can't work around the problems with subbing in, the 'var' approach was the safer place to land.
Nov 6, 2013 at 3:27 AM
Ok, that makes sense. Thanks for the in-depth response. I can definitely understand the performance implications; code organization should not impact performance. Correctness could still be guaranteed via intelligent symbol renaming but I can also understand the argument for generated code clarity. I am planning to use TypeScript in conjunction with Google Closure Compiler (prototype shim code here) so the performance concerns you mentioned are not an issue for me (the compiler safely flattens all module references to top-level symbols so there is no property access overhead).