Question

My team and I are new to TypeScript and NodeJS, and we're having some serious troubles getting this project off the ground. It's meant to be a modularized game asset pipeline written in TypeScript for use with NodeJS (I wasn't involved in the decision making here, and the discussion is still sort-of-ongoing, but we still need a prototype).

We have a good idea of the architecture already, but it's kinda difficult to even get the prototype running, because I just can't seem to manage to wrap my head around how modules work in combination with interfaces and external dependencies. Some of our components also need to use NodeJS or other library modules (e.g. NodeJS.fs for reading/writing files), and we'd prefer to be able to use ///reference to add the appropriate d.ts files to keep type safety at development time.

An example of the supposed project structure is:

/pipeline
    /log
        - ILog.d.ts
        - StdOutLog.ts
        - FileLog.ts
        - ...
    /reader
        - IReader.d.ts
        - TextReader.ts
        - ...
    /parser
        - IParser.d.ts
        - XmlParser.ts
        - JsonParser.ts
        - ...
    /task
        - ITask.d.ts
        - JsTask.ts
        - CliTask.ts
        - ...
    /...

The way we want to use it is that we provide default implementations for our interfaces (to cover the basics, e.g. running a console command, logging to a file or a stream, reading JSON and XML configs, ...), as well as the interfaces themselves so that other teams can create their own extensions (e.g. class GitTask extends CliTask to encapsulate repository operations, or class JpgReader implements IReader).

On the calling side, in a different project/runner app, it should work like this:

import pipeline = require('pipeline');

//...

var log = new pipeline.log.FileLog();
log.info("filelog started");

var parser = new pipeline.parser.XmlParser();
parser.parse(somexmldata);
log.info("parsing XML");

// ...

I'm very likely just doing it wrong (tm), but I feel that it's not easily possible to do with TypeScript what we want to do, especially considering that e.g. the definition for the log component can easily have several interfaces and also enums (factory, logger, logitem, logtarget, loglevel). As far as I understand, NodeJS modules need to be a single JS file, but using even a single import turns your module into an external module and those won't compile into a single file anymore, so that seems like a pretty steep roadblock to me.

Is there a way to realize that strucute and intended usage with TypeScript targeting NodeJS? If yes, what would the files need to look like (especially regarding the module hierarchy and component FQNs, e.g. pipeline.log.X, pipeline.task.Y, etc; how would I properly use module and export for it)? If no, what are suitable alternatives to achieve our goal?


[Update] I've refactored my prototype according to basarat's suggestion, and it already looks much better than before. However, I've encountered a compile error when using one class from another:

error TS2095: Could not find symbol 'LogItem'.

LogItem is a class defined in /log and it's used by StdOutLog.ts in the same folder. IntelliJ gives me multiple definitions for it (obviously, one the LogItem class itself, and the other in /log's index file where it's exported). I've tried using the module notation log.LogItem, but that didn't work. Adding all default implementation files to the global declarations file didn't work either. [/Update]

[Update2] Here's some more code around where the error happens. I don't get any errors/markings in IntelliJ, only when running the grunt-ts task.

// src/log/LogItem.ts:

///<reference path='../pipeline.d.ts'/>
class LogItem implements ILogItem {
    // ...

    constructor(level:LogLevel, message:string) {
        // ...
    }

    public static create(level:LogLevel, message:string):ILogItem {
        return new LogItem(level, message);
    }

    // ...
}

export = LogItem;

// src/log/Log.ts:

///<reference path='../pipeline.d.ts'/>
class Log implements ILog {
    // ...

    private getLogItem(level:LogLevel, message:string):ILogItem {
        return LogItem.create(level, message); // <-- that's where I get the "symbol not found" error
    }

    // ...
}

export = Log;

// src/pipeline.d.ts:

///<reference path="../typings/node/node.d.ts" />

//grunt-start
/// <reference path="pipeline.ts" />
/// <reference path="log/Log.ts" />
/// <reference path="log/LogFactory.ts" />
/// <reference path="log/LogItem.ts" />
/// <reference path="log/StdLogEmitter.ts" />
/// <reference path="log/log.d.ts" />
/// <reference path="log/log.ts" />
/// <reference path="parser/JsonParser.ts" />
/// <reference path="parser/parser.d.ts" />
/// <reference path="parser/parser.ts" />
//grunt-end

[/Update2]

Was it helpful?

Solution

Reference all your .d.ts files in a global globals.d.ts file that has ///<reference tags to all your individual references + vendor.d.ts (e.g. node.d.ts) files.

/pipeline
    /globals.d.ts
    /log
        - ILog.d.ts
        - StdOutLog.ts
        - FileLog.ts
        - ...
    /reader
        - IReader.d.ts
        - TextReader.ts
        - ...
    /parser
        - IParser.d.ts
        - XmlParser.ts
        - JsonParser.ts
        - ...
    /task
        - ITask.d.ts
        - JsTask.ts
        - CliTask.ts
        - ...
    /...

This keeps you from constantly referencing .d.ts files.

Now each typescript file will export some class e.g. XmlParser.ts:

/// <reference path='../globals.d.ts'/>
class XMLParser implements IParser{
}
export = XMLParser;

Each folder also has an index.ts that imports and the exports all the classes in that folder External module style:

export import XmlParser = require('./xmlParser'); 
// so on 

Same for the one level up (pipeline/index.ts):

export import parser = require('./parser/index');

Now if you import pipeline/index.ts you code will work as expected :

import pipeline = require('./pipeline/index');


var parser = new pipeline.parser.XmlParser();
parser.parse(somexmldata);
log.info("parsing XML");

Note : Grunt-ts can create these import / export statements for you so that you do not need to worry about file paths : https://github.com/grunt-ts/grunt-ts/issues/85

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top