Support both Node.js and browser JS in one TypeScript file

Support both JS and Node.js in one TypeScript file Support both JS and Node.js in one TypeScript file

TypeScript allows for better JavaScript development. JavaScript is getting useful in more domains. But different systems require different ways of handling modules and exposing features. TypeScript solves this by compiling differently when a target is specified.

But what if you need a TypeScript script that supports both vanilla browser JS and Node.js? What if you need to expose 10+ classes?

Exposure

Creating a different way of handling the exposure of objects might be the solution. I've created a small script to help. Let's say we need to expose class EventDispatcher and method createEventDispatcher. Add them both to the exportables array. The script will try to detect the right way of exposing the objects.

/* modules, require and stuff like that */
declare var define: any;
declare var module: any;

(function () {

    let exportables = [EventDispatcher, createEventDispatcher];

    // Node: Export function
    if (typeof module !== "undefined" && module.exports) {
        exportables.forEach(exp => module.exports[nameof(exp)] = exp);
    }
    // AMD/requirejs: Define the module
    else if (typeof define === 'function' && define.amd) {
        exportables.forEach(exp => define(() => exp));
    }
    //expose it through Window
    else if (window) {
        exportables.forEach(exp => (window as any)[nameof(exp)] = exp);
    }

    function nameof(fn: any): string {
        return typeof fn === 'undefined' ? '' : fn.name ? fn.name : (() => {
            let result = /^function\s+([\w\$]+)\s*\(/.exec(fn.toString());
            return !result ? '' : result[1];
        })();
    }

} ());

With a declare for module and defined you don't have to add extra typings to your project for NodeJS of AMD. The right method will be available when the JS is compiled and executed.

The exportables array is wrapped in an anonymous function to prevent collisions with other scripts.

Node.js support

Node uses module.exports to expose methods and classes to other scripts. The script will try to resolve the name of each object in the exportable array and add it as a property to the modules.exports.

To keep things strongly typed, consider defining an interface for the exports:

interface IStronglyTypedEvents {
    EventDispatcher: <TSender, TArgs>() => EventDispatcher<TSender, TArgs>;
    createEventDispatcher: <TSender, TArgs>() => EventDispatcher<TSender, TArgs>;
}

Use the interface with the require:

let events = require('./StronglyTypedEvents') as IStronglyTypedEvents;
let dispatcher = events.createEventDipatcher<MyClass, string>();

The events variable is now strongly typed.

AMD support

The script supports AMD. It registers each object as a function through the require method.

Vanilla

Not using modules? Great! Just add a reference & use it:

/// <reference path="../StronglyTypedEvents.ts" />
let dispatcher = new EventDispatcher<MyClass, string>();

If all else fails, it will add the object to the window object. This is especially useful when you use your own module system or way of information hiding.

Example: Mocha, Chai in browser

So I'm using Mocha, Chai and NPM to test. Everything works fine with an npm test. But I want the tests to be available in the browser as well. I don't like to compile differently and I should not have too. The following JavaScript combines both worlds in one test solution:

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

'use strict';

var r = typeof require !== 'undefined';
var expect: Chai.ExpectStatic = r ? require('chai').expect : (window as any).chai.expect;
var _e: IStronglyTypedEvents = r ? require('../StronglyTypedEvents') : window;
expand_less