Unexported members accessible using merged declaration and export assignment

Topics: Language Specification
Sep 24, 2013 at 2:55 AM
While improving the express.d.ts file of DefinitelyTyped I noticed something I thought was a bit weird. Not sure if it's according to spec or if it is a bug.

Since express needs to be callable and at the same time it's a namespace. I am using the merged declarations with export assignment "trick". The following is a reduced example:
// e.d.ts
declare module "e" {
    function inner_e(): inner_e.Foo;

    module inner_e {
        interface Foo {
            blargh: string;
        }

        interface Bar {
            toString(): string;
            foo_to_bar: (foo: Foo) => Bar;
        }
    }

    export = inner_e;
}
//main.js
///<reference path="e.d.ts" />

import e = require('e');

var something = e();

function f(x: e.Foobar) {
    console.log(x.toString());
}
So everything works properly, types are being checked. e is both callable and a container with types at the same time. But I thought that the members of inner_e would have to be explicitly exported to be usable as in e.Foobar in f's prototype. Is this some intended behavior since it is an ambient module or am I missing something?
Developer
Sep 24, 2013 at 7:47 PM
Yeah, as you suspected this is because the module declaration for inner_e is ambient. From 12.1.5 of the spec:

"Except for ImportDeclarations, AmbientModuleElements always declare exported entities regardless of whether they include the optional export modifier."

The thinking being that if you're describing a set of types/values to be imported, why would you describe unusable (ie non-exported) ones? Either inner_e has those interfaces exported (meaning they would be in your ambient module declaration) or you wouldn't bother to write them at all since they'd be invisible to the user of the module. So you can be explicit and add the export keyword but it is implicitly there even if omitted.