Tsonic GitHub

Tsonic

Tsonic is a TypeScript to C# compiler that produces native executables via .NET NativeAOT. Write TypeScript, get fast native binaries. Opt into @tsonic/js (JavaScript runtime APIs) and @tsonic/nodejs (Node-style APIs) when you want them.

Why Tsonic?

Tsonic lets TypeScript/JavaScript developers build fast native binaries for x64 and ARM64:

  • Native binaries (no JS runtime).
  • .NET standard library: use the .NET runtime + BCL (files, networking, crypto, concurrency, etc.).
  • Optional JS/Node APIs when you want them: @tsonic/js (JavaScript runtime APIs) and @tsonic/nodejs (Node-style APIs).
  • Still TypeScript: your code still typechecks with tsc. Tsonic also adds CLR-style numeric types like int, uint, long, etc. via @tsonic/core/types.js.
  • Better security: you build on a widely used runtime and standard library with regular updates.

Tsonic targets the .NET BCL (not Node’s built-in modules). If you want JavaScript-style APIs, opt into @tsonic/js. If you want Node-like APIs, opt into @tsonic/nodejs.

Why C# + NativeAOT?

Tsonic compiles TypeScript to C#, then uses the standard CLR NativeAOT pipeline (dotnet publish) to produce native binaries.

TypeScript maps well to C#/.NET:

  • Classes, interfaces, generics: translate naturally to CLR types.
  • Async/await: TS async maps cleanly to Task/ValueTask.
  • Iterators and generators: map to C# iterator patterns.
  • Delegates/callbacks: map to Action/Func without inventing a new runtime ABI.

NativeAOT produces single-file, self-contained native executables.

Details live in the docs: /tsonic/build-output/ and /tsonic/architecture/pipeline/.

Key Features

  • TypeScript to Native: Compile TypeScript directly to native executables
  • Optional JS/Node compatibility: @tsonic/js (JS runtime APIs) and @tsonic/nodejs (Node-style APIs)
  • Direct .NET Access: Full access to .NET BCL with native performance
  • NativeAOT Compilation: Single-file, self-contained executables
  • Full .NET Interop: Import and use any .NET library
  • ESM Module System: Standard ES modules with .js import specifiers

Installation

npm install -g tsonic

Prerequisites:

Quick Start

Initialize a New Project

mkdir my-app && cd my-app

# Basic project
tsonic project init

# Or: include JavaScript runtime APIs (console, JSON, timers, etc.)
tsonic project init --js

# Or: include Node-style APIs (fs, path, crypto, http, etc.)
tsonic project init --nodejs

This creates:

  • src/App.ts - Entry point
  • tsonic.json - Configuration
  • package.json - With build scripts

Build and Run

npm run build    # Build native executable
./out/app        # Run it

# Or build and run in one step
npm run dev

Example Program

// src/App.ts
import { Console } from "@tsonic/dotnet/System.js";

export function main(): void {
  const message = "Hello from Tsonic!";
  Console.writeLine(message);

  const numbers = [1, 2, 3, 4, 5];
  Console.writeLine(`Numbers: ${numbers.length}`);
}

Using .NET APIs (BCL)

import { Console } from "@tsonic/dotnet/System.js";
import { File } from "@tsonic/dotnet/System.IO.js";
import { List } from "@tsonic/dotnet/System.Collections.Generic.js";

export function main(): void {
  // File I/O
  const content = File.readAllText("./README.md");
  Console.writeLine(content);

  // .NET collections
  const list = new List<number>();
  list.add(1);
  list.add(2);
  list.add(3);
  Console.writeLine(`Count: ${list.count}`);
}

Examples

LINQ extension methods (where, select)

import { List } from "@tsonic/dotnet/System.Collections.Generic.js";
import type { ExtensionMethods as Linq } from "@tsonic/dotnet/System.Linq.js";

type LinqList<T> = Linq<List<T>>;

const xs = new List<number>() as unknown as LinqList<number>;
xs.add(1);
xs.add(2);
xs.add(3);

const doubled = xs.where((x) => x % 2 === 0).select((x) => x * 2).toList();
void doubled;

JSON with the .NET BCL (System.Text.Json)

import { Console } from "@tsonic/dotnet/System.js";
import { JsonSerializer } from "@tsonic/dotnet/System.Text.Json.js";

type User = { id: number; name: string };

const user: User = { id: 1, name: "Alice" };
const json = JsonSerializer.serialize(user);
Console.writeLine(json);

const parsed = JsonSerializer.deserialize<User>(json);
if (parsed !== undefined) {
  Console.writeLine(parsed.name);
}

JavaScript runtime APIs (@tsonic/js)

First, enable JSRuntime APIs:

# New project
tsonic project init --js

# Existing project
tsonic add js

Then write:

import { console, JSON } from "@tsonic/js";

export function main(): void {
  const value = JSON.parse<{ x: number }>('{"x": 1}');
  console.log(JSON.stringify(value));
}

Node-style APIs (@tsonic/nodejs)

First, enable Node-style APIs:

# New project
tsonic project init --nodejs

# Existing project
tsonic add nodejs

Then write:

import { console, path } from "@tsonic/nodejs";

export function main(): void {
  console.log(path.join("a", "b", "c"));
}

Minimal ASP.NET Core API

First, add the shared framework + bindings:

tsonic add framework Microsoft.AspNetCore.App @tsonic/aspnetcore

Then write:

import { WebApplication } from "@tsonic/aspnetcore/Microsoft.AspNetCore.Builder.js";

export function main(): void {
  const builder = WebApplication.createBuilder([]);
  const app = builder.build();

  app.mapGet("/", () => "Hello from Tsonic + ASP.NET Core!");
  app.run();
}

tsbindgen (CLR Bindings Generator)

Tsonic doesn’t “guess” CLR types from strings. It relies on bindings packages generated by tsbindgen:

  • Given a .dll (or a directory of assemblies), tsbindgen produces:
    • ESM namespace facades (*.js) + TypeScript types (*.d.ts)
    • bindings.json (namespace → CLR mapping)
    • internal/metadata.json (CLR metadata for resolution)
  • Tsonic uses these artifacts to resolve imports like:
    • import { Console } from "@tsonic/dotnet/System.js"

Tsonic can run tsbindgen for you:

# Add a local DLL (auto-generates bindings if you omit the types package)
tsonic add package ./path/to/MyLib.dll

# Add a NuGet package (auto-generates bindings for the full transitive closure)
tsonic add nuget Newtonsoft.Json 13.0.3

# Or use published bindings packages (no auto-generation)
tsonic add nuget Microsoft.EntityFrameworkCore 10.0.1 @tsonic/efcore

CLI Commands

Command Description
tsonic project init Initialize new project
tsonic generate [entry] Generate C# code only
tsonic build [entry] Build native executable
tsonic run [entry] Build and run
tsonic add js Add @tsonic/js + JSRuntime DLLs
tsonic add nodejs Add @tsonic/nodejs + NodeJS DLLs
tsonic add package <dll> [types] Add a local DLL + bindings
tsonic add nuget <id> <ver> [types] Add a NuGet package + bindings
tsonic add framework <ref> [types] Add a FrameworkReference + bindings
tsonic restore Restore deps + bindings
tsonic pack Create a NuGet package

Common Options

Option Description
-c, --config <file> Config file (default: tsonic.json)
-o, --out <name> Output name (binary/assembly)
-r, --rid <rid> Runtime identifier (e.g., linux-x64)
-O, --optimize <level> Optimization: size or speed
-k, --keep-temp Keep build artifacts
-V, --verbose Verbose output
-q, --quiet Suppress output

Configuration (tsonic.json)

{
  "$schema": "https://tsonic.org/schema/v1.json",
  "rootNamespace": "MyApp",
  "entryPoint": "src/App.ts"
}

Project Structure

my-app/
├── src/
│   └── App.ts           # Entry point (exports main())
├── tsonic.json          # Configuration
├── package.json         # NPM package
├── generated/           # Generated C# (gitignored)
└── out/                 # Output executable (gitignored)

Naming Modes

Tsonic supports two binding/name styles:

  • Default: JavaScript-style member names (Console.writeLine)
  • --pure: CLR-style member names (Console.WriteLine)
tsonic project init --pure

Npm Workspaces (Multi-Assembly Repos)

Tsonic projects are plain npm packages, so you can use npm workspaces to build multi-assembly repos (e.g. @acme/domain + @acme/api).

  • Each workspace package has its own tsonic.json and produces its own output (dist/ for libraries, out/ for executables).
  • Build workspace dependencies first (via npm run -w <pkg> ...) before building dependents.
  • For library packages, you can generate tsbindgen CLR bindings under dist/ and expose them via npm exports; Tsonic resolves imports using Node resolution (including exports) and locates the nearest bindings.json.

See /tsonic/dotnet-interop/ for the recommended dist/ + exports layout.

Documentation

Type Packages

Package Description
@tsonic/globals Base types (Array, String, iterators, Promise)
@tsonic/core Core types (int, float, etc.)
@tsonic/dotnet .NET BCL type declarations
@tsonic/js JavaScript runtime APIs (JS semantics on .NET)
@tsonic/nodejs Node-style APIs implemented in .NET

License

MIT