ts2hx

Transpile Typescript to Haxe.

Usage no npm install needed!

<script type="module">
  import ts2hx from 'https://cdn.skypack.dev/ts2hx';
</script>

README

ts2hx

Compile/Transpile typescript code to ready-to-run haxe code.

Why?

I really like Haxe (http://haxe.org) and openFL (http://openfl.org) projects but I couldn't find a proper Haxe IDE on Mac that could suit my needs completely. However, Typescript is officially supported by several IDEs (Webstorm/IntelliJ, Visual Studio), making it very convenient to use. Then came the idea of writing a Typescript to Haxe transpiler. Typescript got ECMAScript roots and static typing which are pretty similar to Haxe.

Using ts2hx, I am able to compile a pixi.js-based (http://pixijs.com) HTML5 app written in Typescript and make it work at almost native speed on mobile devices with openFL CPP target (yes, it becomes possible to compile Typescript to C++!). The only code that needs to be re-written in Haxe is the platform-specific code (use openFL API instead of pixi.js etc...). If all the platform-specific code is properly encapsuled in reusable classes, the rest of the code (all the app logic) can become 100% portable and compilable to valid haxe code.

That said, keep in mind this project is still experimental.

How to use

Install package

npm install ts2hx

Example

var ts2hx = require('ts2hx');

// Compile typescript code
var haxeCode = ts2hx([
    "class FooClass {",
    "    constructor(public name:string) {",
    "        console.log('Hello, my name is '+this.name);",
    "    }",
    "}"
].join("\n"));

// Log haxe output
process.stdout.write(haxeCode);

Expected output:

package;

class FooClass {

    public var name:String;

    public function new(name:String) {
        this.name = name;
        trace('Hello, my name is ' + this.name);
    }

}

Command Line Interface

You can install the cli command:

npm install -g ts2hx

Then run it:

ts2hx someFile.ts > result.hx

You can also build a full directory of typescript files (recommended):

ts2hx --typescript dir/to/typescript/files --destination dir/to/compiled/haxe/files

When building a full directory, ts2hx will be able to perform additional tasks:

  • Add override keyword on methods overriding a parent class method (when the parent class is in the project)

  • Add in the final directory a support file Ts2Hx.hx required in some cases to make transpiled files work fine.

  • Replace some compiled files with original haxe files if needed (using --haxe option), allowing to use alternative implementations of specific classes in Haxe.

Compilation rules

When compiling a typescript file, ts2hx performs conversions to make the haxe code behave the same as its typescript counterpart.

Core types

Typescript

var foo:number = 1;
var bar:string = "Hello";
var baz:boolean = true;
var qux:any = { some: 'values' };

Haxe

var foo:Float = 1;
var bar:String = "Hello";
var baz:Bool = true;
var qux:Dynamic = { some: 'values' };

Integers

The Int type doesn't exist in typescript. However, it is still possible to transpile number to Int when transpiling thanks to type inference or a custom typescript interface integer.

Add the integer type in typescript

Add a typescript definition file to your project (integer.d.ts)

interface integer extends number {}

You can then use integer in your typescript code while still manipulating numbers in compiled javascript:

Typescript

var foo:integer;
var bar:number;

Haxe

var foo:Int;
var bar:Float;

Integers by type inference

If you really don't want to use integer interface, you can still create haxe Int using type inference:

Typescript

var foo = 1;
var bar = 1.0;

Haxe

var foo:Int = 1;
var bar:Float = 1.0;

Enum

Typescript

enum EnumExample {
    VALUE1,
    VALUE2,
    VALUE3,
    VALUE4,
    VALUE5
}

Haxe

enum EnumExample {
    VALUE1;
    VALUE2;
    VALUE3;
    VALUE4;
    VALUE5;
}

Switchs

The break keyword in switch statements doesn't exist in haxe. Fall-through cases are converted to comma-separated cases.

Typescript

switch (value) {
    case 1:
    case 2:
        console.log('value is 1 or 2');
        break;
    case 3:
        console.log('value is 3');
        break;
    default:
        console.log('value is '+value);
}

Haxe

switch (value) {
    case 1, 2:
        trace('value is 1 or 2');
    case 3:
        trace('value is 3');
    default:
        trace('value is ' + value);
}

Interfaces

Typescript

interface MyInterface {
    x:number;
    y:number;
    foo():void;
}

Haxe

interface MyInterface {
    public var x:Float;
    public var y:Float;
    public function foo():Void;
}

Classes

Classes are properly converted, including typescript-specific features like getters/setters or properties in constructor signature.

Typescript

class FooClass extends BarClass, BazClass implements QuxInterface {

    private prop1:number;
    static prop2:number = 0;

    constructor(public prop3:number) {
        this.prop1 = 0;
    }

    get prop4():number {
        return this.prop1 + FooClass.prop2;
    }

    set prop4(value:number) {
        this.prop1 = value - FooClass.prop3;
    }

}

Haxe

class FooClass extends BarClass extends BazClass implements QuxInterface {

    private var prop1:Float;

    static public var prop2:Float = 0;

    public var prop3:Float;

    public function new(prop3:Float) {
        this.prop3 = prop3;
        this.prop1 = 0;
    }

    public var prop4(get, set):Float;

    public function get_prop4():Float {
        return this.prop1 + FooClass.prop2;
    }

    public function set_prop4(value:Float):Float {
        this.prop1 = value - FooClass.prop3;
        return value;
    }

}

Generics

Typescript generics are converted to haxe generics. The @:generic macro is added automatically in haxe code.

Typescript

class GenericClassExample<T> {

    constructor(content:T) {
    }
    
}

class GenericClassExample2<T extends InterfaceA> {

    constructor(private content:T) {
    }
}

Haxe

@:generic
class GenericClassExample<T> {

    public function new(content:T) {
    }

}

@:generic
class GenericClassExample2<T:InterfaceA> {

    private var content:T;

    public function new(content:T) {
        this.content = content;
    }

}

Closures

Typescript's double-arrow closures are converted to Haxe, ensuring this is still referencing the parent context.

Typescript

class Foo {

    public name:string = 'Foo';

    constructor() {

        var someClosure = () => {
            this.name += ' Bar';
        }

    }
}

Haxe

class Foo {

    public var name:String = 'Foo';

    public function new() {
        var __this = this;
        var someClosure = function() {
            __this.name += ' Bar';
        }
    }

}

Logs

console.log becomes trace

Typescript

console.log('hello');

Haxe

trace('hello');

setTimeout/setInterval

setTimeout and setInterval are converted to Ts2Hx.setTimeout and Ts2Hx.setInterval (requires Ts2Hx.hx support file).

Typescript

setTimeout(function() {}, 1000);
setInterval(function() {}, 1000);

Haxe

Ts2Hx.setTimeout(function() {}, 1000);
Ts2Hx.setInterval(function() {}, 1000);

Objects

Typescript objects are converted to haxe anonymous structures. The delete keyword and brackets access are converted to their closest equivalent in haxe (requires Ts2Hx.hx support file).

Typescript

var dict:any = {
    foo: 'bar',
    baz: 'qux'
};
dict['foo'] = 'baz';
dict.baz = 1234;
dict['foo' + dict.foo] = 'qux';
delete dict.baz;

Haxe

var dict:Dynamic = {
    foo: 'bar',
    baz: 'qux'
};
dict.foo = 'baz';
dict.baz = 1234;
Ts2Hx.setValue(dict, 'foo' + dict.foo, 'qux');
Reflect.deleteField(dict, 'baz');

For loops with incrementor

for loops with incrementor don't exist in haxe. They are converted to while loops.

Typescript

for (var i = 0, len = 12; i < len; i++) {
    console.log("for iteration #"+i);
}

Haxe

var i:Int = 0, len:Int = 12;
while (i < len) {
    trace("for iteration #" + i);
    i++;
}

For loops over object

for loops can be used to iterate over an object's keys.

Typescript

for (var key:string in someObject) {
    console.log('key: ' + key);
    console.log('value: ' + someObject[key]);
}

Haxe

for (key in Reflect.fields(someObject)) {
    trace('key: ' + key);
    trace('value: ' + Ts2Hx.getValue(someObject, key));
}

Call of forEach method of an Array

Array.forEach are transpiled to the closed equivalent in haxe (requires Ts2Hx.hx support file).

Typescript

['item1', 'item2'].forEach(function(value) {
    console.log(value);
});

Haxe

Ts2Hx.forEach(['item1', 'item2'], function(value) {
    trace(value);
});

JSON parsing and dumping

JSON.parse and JSON.stringify are transpiled to the closed equivalent in haxe (requires Ts2Hx.hx support file).

Typescript

var jsonValue:any = JSON.parse('{"a":1, "b": ["c", "d", "e"]}');
var jsonString:string = JSON.stringify(jsonValue);

Haxe

var jsonValue:Dynamic = Ts2Hx.JSONparse('{"a":1, "b": ["c", "d", "e"]}');
var jsonString:String = Ts2Hx.JSONstringify(jsonValue);

More examples

You can find more examples in the examples/ directory, such as try/catch, do/while etc...