4

Closed

`export =` not usable without claiming a name in the global namespace

description

Since only one export = is allowed it is not possible to use exports = with lots of interfaces when defining external only modules. e.g.
declare module 'foo'{
    
    interface Foo{
        ():string;
    }
    
    var foo:Foo;  // Error exported variable foo is using private type Foo
    export = foo;
}

Workaround Simple

If all you need is a single interface you can replace the interface with an inline declaration i.e. for the above example :
declare module 'foo'{
    
    var foo: {
        ():string;
    }
    
    export = foo:
}

Workaround Complex

If you have multiple interfaces you need to export e.g. :
declare module 'foo'{
    interface Bar{
        bar: string;
    }
    interface Foo{
        ():Bar;
    }
    
    var foo:Foo; // ERROR
    export = foo;
}
putting everything inline is not a option (e.g multiple things might return Bar for given example). In this case you are forced to claim a name in the global namespace (an internal module name):
declare module Foo{  // Claim a name in the global namespace
    export interface Bar{
        bar: string;
    }
    export interface Foo{
        ():Bar;
    }
}

declare module 'foo'{
    var foo:Foo.Foo;
    export = foo;
}

Solution

I do not have a good solution and am open for suggestions
Closed Apr 9, 2014 at 2:35 AM by danquirk

comments

Bartvds wrote Apr 6, 2014 at 11:25 PM

+1

This is one of a few patterns where the external module system can't quite encapsulate everything and we need to bail into global space. The workaround of adding an internal module works in a single case but it gets problematic as it doesn't scale because it claims random words in the combined application and module-name vocabularies.

Eg: a workaround like this breaks one the the main benefits of using external modules. If you look at npm and the crazy names people use for libraries: imagine the fun when you have an existing project and need to add some library that has the name of one of your own types, modules or popular identifiers.

yortus wrote Apr 7, 2014 at 3:18 AM

A possible solution:
declare module 'foo' {
    import Foo = require('foo/Foo');
    
    var foo:Foo; // Works
    export = foo;
}

declare module 'foo/Bar' {
    interface Bar {
        bar: string;
    }
    export = Bar;
}

declare module 'foo/Foo' {
    import Bar = require('foo/Bar');
    interface Foo{
        (): Bar;
    }
    export = Foo;
}

basarat wrote Apr 7, 2014 at 4:01 AM

@yortus that pollutes the external module namespace (e.g. require('foo/Bar')) so not a solution.

yortus wrote Apr 7, 2014 at 4:15 AM

@basarat it's namespaced within the top-level foo, not separately. CommonJS and AMD would both look for it within foo using their own resolution methods.

If foo is considered 'taken' at the global namespace level, then foo/X, foo/Y/Z and all other such names should be considered 'taken' as well, so there is no additional pollution in any meaningful sense.

Bartvds wrote Apr 7, 2014 at 2:32 PM

@yortus It still creates artefacts to work around a language limitation. Less evil as it is more isolated to jsut external module-name space but is is a bit wonky.

If the module is a real external module 'foo' (from npm or whatever) then ideally we shouldn't have to hack using module Foo nor module 'foo/Foo' to get it working.

yortus wrote Apr 8, 2014 at 2:07 AM

Okaaay if you want to be academic. I thought this might be about reducing global namespace pollution, especially going forward on DefinitelyTyped.

Here's a practical example. On DT, node-fibers.d.ts includes the declarations:
interface Fiber {
    reset: () => any;
    run: (param?: any) => any;
    throwInto: (ex: any) => any;
}
declare module "fibers" {
    function Fiber(fn: Function): Fiber;
    module Fiber {
        export var current: Fiber;
        export function yield(value?: any): any
    }
    export = Fiber;
}
This is a real case of what this issue describes. The interface Fiber pollutes the global namespace. But if you move it into the module "Fibers" declaration, you get the exported ... is using private type error message. And you can't export the interface because there is already the export =.

Here's the same code in an alternative form:
declare module "fibers/Fiber" {
    interface Fiber {
        reset: () => any;
        run: (param?: any) => any;
        throwInto: (ex: any) => any;
    }
    export = Fiber;
}
declare module "fibers" {
    import Fiber = require("fibers/Fiber");
    function FiberStatic(fn: Function): Fiber;
    module FiberStatic {
        export var current: Fiber;
        export function yield(value?: any): any
    }
    export = FiberStatic;
}
By claiming the global module name "fibers", you are effectively claiming all slash-delimited descendent names (at least in every CommonJS/AMD implementation I can think of, including node.js and requirejs). So the claim to "fibers" already covers "fibers/Fiber". So no pollution beyond "fibers" which you are claiming anyway. This version is a practical solution to the problem described in this issue.

This is purely used within the type declaration file at TypeScript compile-time. User code would be no different, using import Fiber = require("fibers") as before. Anyone silly enough to do require('Fibers/anything') would get the same runtime error message they got before, as for any library.

And it's hardly a hack. Look a little further down in node-fibers.d.ts and you'll see:
declare module "fibers/future" {

    class Future {
        constructor();
        ...
... as per how the fibers module is actually defined. Using namespaced module identifiers may not be common, but its perfectly valid and used to good effect by some libraries out there.

Bartvds wrote Apr 8, 2014 at 3:51 AM

@yortus That is still adding things that aren't there. The "fibers/future" module actually exists, but this "fibers/Fiber" is another construct to bypass the language.

And this pattern has been seen before (but using external modules that has names with underscore prefix).

I would hope to keep the DT discussion on DT where we had it going? I personally was hoping to get some input on this as abstract general case.

yortus wrote Apr 8, 2014 at 4:23 AM

@bartvds sorry, was just trying to help since @basarat ended with:
I do not have a good solution and am open for suggestions
and I know you guys are mostly concerned with DT issues.

We can keep it there and I'll make no further comments here, beyond the following:
That is still adding things that aren't there
That's exactly what TypeScript is for. Adding type information that is there at compile-time and not there at runtime.
And this pattern has been seen before (but using external modules that has names with underscore prefix)
This is not the same thing at all. Slashes have special meaning in CommonJS/AMD module identifiers. Underscores create unrelated names and hence additional global pollution.

danquirk wrote Apr 9, 2014 at 2:35 AM

Definitely hear you that this pattern can feel a little awkward at times. That said, it does allow you to model the patterns that are necessary with JavaScript. I'm going to close this issue though since this seems better suited to a forum post for now where more people will see it and we all can discuss the current organization people are using and any other alternatives people feel would be an improvement. You may want to look at the amd-dependency /// method as well.