Module, Interface and class interaction: improvement proposal

Topics: General, Language Specification
Mar 2, 2014 at 11:18 PM
A nice thing in TypeScript is that you can add static members to a class by a later module definition, and you can refer to things in that module definition too, e.g. interfaces, helper functions etc. e.g.
// Interface for requesting a new foo object.
class Foo {
  constructor (public x: Foo.Request) {
    Foo.Helper(x);
  }
  //... other stuff for Foo...
}

// static module addition of
module Foo {
  export interface Request {
    a: string;
    b: string;
  }

  // Aux functions to help the class... can also be done as static private in
  // the class, but problem: sometime more convenient to have in a separate file.
  export function Helper(x: Request) {
   // ... do helpful stuff...
  }

  // helper stuff you don't want in the public
  // ...
  // Export helper function, e.g.:
  export function makeTrivialFoo(x:string) { return new Foo({a: x, b:x}); }
}

var foo = Foo.makeTrivialFoo("a");
This lets you hide stuff you don't want to be public in an anonymous closure. Cool.

However, it would be nicer to not have to write Foo.Request for the Foo class. Moreover, sometimes it's helpful to define some auxiliary function related to a module before the class. e.g. I'd really like to be able to write it like this:
// static module addition of
module Foo {
  export interface Request {
    a: string;
    b: string;
  }
  // Aux functions to help the class... can also be done as static private in
  // the class, but problem: sometime more convenient to have in a separate file.
  export function Helper(x: Request) {
   // ... do helpful stuff...
  }
}

// Interface for requesting a new foo object.
class Foo {
  constructor (public x: Request) { Helper(x); }
  //... other stuff for Foo...
}

// static module addition of
module Foo {
  // helper stuff you don't want in the public
  // ...
  // Export helper function, e.g.:
  export function makeTrivialFoo(x:string) { return new Foo({a: x, b:x}); }
}

var foo = Foo.makeTrivialFoo("a");
But I can't because typescript gives a type-error about the symbol Foo. It's strange that the order would change the symbol defined error :(

It would make code a good deal cleaner. You don't need prefixes in the class definition. An alternative thing would be allow definitions of interfaces in a class.

Thoughts?
Developer
Mar 3, 2014 at 7:51 PM
The order of merging declarations (class + module) matters because of the underlying JavaScript generated. At that level you're creating a function named Foo (the constructor function for the class) and then adding additional properties to it (the exported module members). But there's no way to do the reverse. You can't put some properties on a var named Foo and then change Foo to a callable function type. So at the TypeScript level we need to enforce this ordering restriction between classes and modules that would merge. If you use ambient declarations then we relax this restriction since the ambient declaration will not have any code generated for it.
Mar 4, 2014 at 5:46 PM
Is there a way to defer generation until the class declaration? That'd let this happen, and may provide opportunity to generate better code, with more information about Foo available.
Developer
Mar 4, 2014 at 9:30 PM
In theory the compiler could do some whole program analysis to alter emit but we've tried to keep the code gen fairly simple as far as your ability to look at a piece of TypeScript and understand exactly what JS will be emitted for it.
Mar 4, 2014 at 10:30 PM
Edited Mar 5, 2014 at 8:03 PM
Thanks for the explanation!

So to put a class definition after the module, we'd need to make typescript do a slightly different compilation... something like this...
// static module addition of
var Foo;
(function (Foo) {
    // Aux functions to help the class... can also be done as static private in
    // the class, but problem: sometime more convenient to have in a separate file.
    function Helper(x) {
        console.log("helper");
        // ... do helpful stuff...
    }
    Foo.Helper = Helper;
})(Foo || (Foo = {}));

// Interface for requesting a new foo object.
var Foo = (function (_module) {
    function Foo(x) {
        this.x = x;
        _module.Helper(x);
    }

    // Add the previous Foo module to the new function object.
    for(x in _module) { Foo[x] = _module[x]; }

    return Foo;
})(Foo);

(function (Foo) {
    // helper stuff you don't want in the public
    // ...
    // Export helper function, e.g.:
    function makeTrivialFoo(x) {
        return new Foo({ a: x, b: x });
    }
    Foo.makeTrivialFoo = makeTrivialFoo;
})(Foo || (Foo = {}));

var foo = Foo.makeTrivialFoo("a");
That seems like it would work, no?
Mar 5, 2014 at 8:04 PM
Edited Mar 5, 2014 at 8:04 PM
Sorry, previous code was confused; I've updated and tested (new code in the above post). The above seems like a natural way to allow modules defined before a class, and still get all the name-space benefits... what do you think?
Mar 5, 2014 at 8:12 PM
Does this still require reordering the generated code, relative to the order of declarations in the input code?
Mar 5, 2014 at 8:17 PM
No, what's neat about the above is that it still allows 1:1 style incremental compilation, and even separating between files. e.g. the above would be generated for this TS:
// static module addition of
module Foo {
  export interface Request {
    a: string;
    b: string;
  }

  // Aux functions to help the class... can also be done as static private in
  // the class, but problem: sometime more convenient to have in a separate file.
  export function Helper(x: Request) {
   // ... do helpful stuff...
  }
}

// Interface for requesting a new foo object.
class Foo {
  constructor (public x: Foo.Request) {
    Foo.Helper(x);
  }
  //... other stuff for Foo...
}

module Foo {
  // helper stuff you don't want in the public
  // ...
  // Export helper function, e.g.:
  export function makeTrivialFoo(x:string) { return new Foo({a: x, b:x}); }
}

var foo = Foo.makeTrivialFoo("a");
Jun 22, 2014 at 4:23 AM
ping?
Jun 22, 2014 at 7:10 AM
You need to consider that TypeScript is trying to stick to ES6 standards, and emitting the JS in your example may not be the proper behaviour when the native class and module semantics take over in the future. The real question is "How would this look like if coding in ES6?". In fact, TS usually only emits code to bridge the gaps where possible.