Class declaration conflicting with the need to define a class "the javascript way"

Topics: General
Feb 19, 2014 at 10:33 PM
Edited Feb 19, 2014 at 10:51 PM
Hey guys,

I'm currently evaluating TypeScript for quite intensive use in the near future. These days I use Haxe a lot and I got used to the wonderful and lightweight entity-component system that is Ash Framework.

Since there are no TypeScript definitions on DefinitelyTyped for AshJS, I have to make my own as I go.

The problem comes from the fact that nodes, which are user made classes extending ash.core.Node (Ash.Node), need to be created this way in the JS version of Ash:
var CustomNode = Ash.Node.create( { componentVarName: ComponentClass } );
which is a convenience function relying on of Ash.Node.extend. "CustomNode" is meant to be a class btw, not an instance.

The exact problem is that I need a CustomNode type to exist in the eyes of TypeScript compiler, but I need to define CustomNode using Ash.Node.create. How can I reconcile those two ?

You can read on for more information:

Ash.Node.extend, used by Ash.Node.create, comes from a very small API that mimics inheritance. You can find the code here.

So if CustomNode is created as a class, it conflicts with the Ash.Node.create method.

CustomNode signature should be
class CustomNode extends Ash.Node<CustomNode> { componentVarName:ComponentClass; }
In ash.d.ts, I declared Node itself as follows:
declare module Ash {
    export class Node<TNode extends Node<TNode>> {
        // stuff
Then in the code, sometimes you need to retrieve all nodes of a certain type, it is done as follows:
var list:NodeList<CustomNode> = engine.getNodeList<CustomNode>(CustomNode);
with getNodeList declared within ash.d.ts as:
getNodeList<TNode extends Node<TNode>>(nodeClass:any):NodeList<TNode>;
Nodes are never instantiated by the user - he only defines them. They are used internally by the engine to group components by type.

So as I said, the bottom line is that I need the CustomNode type to exist in TypeScript, yet have CustomNode defined with Ash.Node.create.

Thanks A LOT if you can untangle this ! :)
Feb 20, 2014 at 3:48 PM
I'm not familiar with that framework, but a couple things that hopefully will help.

The first is that if you're creating .d.ts files for existing JS libraries, I'd start by using interfaces for pretty much every type first. Once you get it typed that way, you can look into using classes. Interfaces are generally the most flexible and expressive.

For example, an interface that describes the constructor function:
declare module Ash.Node {
                interface Constructable<T> {
                                new(...args: any[]): T;

                function create<T>(prototype: T): Constructable<T>;

var Node = Ash.Node.create({ helloWorld: () => console.log('hi') });
var n = new Node();

The part I'm less sure about is mapping parameters from what you pass into new over to the resulting class. You can do some of this with generics, though the recursively constrained generics like you're using like <TNode extends Node<TNode>>are being removed to simplify the type system, and I'm not sure you need them in this case - at least to get started.
Feb 20, 2014 at 7:20 PM
Edited Feb 20, 2014 at 7:58 PM
Interesting way to define stuff, I'll try to think more in this way. I still don't quite get when to declare stuff as modules, classes or interfaces. For now I tend to default to module = mostly lib packages, class = whenever I need to extend the object's functionality, interface = when TypeScript only needs to instantiate that object, not extend it.

Regarding the issue, the compiler complains whenever I use CustomNode as a type if I use your example. (Error TS2095 "could not find symbol")
Before reading your reply I decided to just fall back to doing whatever Ash.Node.create is doing by hand, which means defining CustomNode this way:
class CustomNode extends Ash.Node<CustomNode>
    public componentVarName: ComponentClass;
    public types: { componentVarName: typeof ComponentClass; };
CustomNode.prototype.types = {componentVarName: ComponentClass};
CustomNode.prototype.componentVarName = null;
It works without compiler error but I guess that ideally I shouldn't use prototype in TypeScript :)

Regarding recursive generics, I thought it described the situation properly. The main Ash.Node class has members of type T, but these must extend Node. I'm not sure that code completion works correctly when I access those members, but the compiler accepts it. I suppose I'll simplify the defs with a bunch of "any" when v0.9.7 is out :)

By the way, do you know a way to define the type of nodeClass here ? This is defined inside an "export class".
getNodeList<TNode extends Node<TNode>>(nodeClass:any):NodeList<TNode>;
"any" is supposed to be "typeof TNode", but that's incorrect use of generics (symbol not found). Just curious. The solution probably necessitates to rewrite that declaration completely...
Feb 20, 2014 at 7:23 PM
Edited Feb 20, 2014 at 7:26 PM
Some others can probably chime in with their experiences but I'll echo what Jonathan said. When I was using TypeScript with a JS library that did inheritance with this sort of pattern (the ImpactJS game engine) I found that it was best to stick with interfaces rather than classes for the most part.

To your second post: Interfaces can be extended just as classes can. Modules are containers (of types, values and/or other containers), while classes and interfaces are used to describe the shape of types (and their implementation in the case of classes).

The typeof operator only operates on values, not types. TNode is a type in this case.