tsrun
A minimal TypeScript runtime in Rust for embedding in applications.
Overview
tsrun is designed for configuration files where you want the full benefits of TypeScript in your editor: autocompletion, type checking, and error highlighting. The runtime executes TypeScript directly without transpilation, using a register-based bytecode VM.
Why TypeScript for configs?
- IDE autocompletion for your config schema
- Catch errors before runtime with type checking in your editor
- Native support for enums, interfaces, and type annotations
- No Node.js dependency - embed directly in your application
Features
TypeScript Support
- Enums - Native support with numeric and string enums, including reverse mappings
- Type Annotations - Full parsing of types, interfaces, type aliases, and generics
- Decorators - Class, method, property, and parameter decorators
- Namespaces - TypeScript namespace declarations
- Parameter Properties -
constructor(public x: number)syntax support - Type Assertions - Both
x as Tand<T>xsyntaxes
JavaScript Features
- ES Modules - Full import/export support with step-based module loading
- Async/Await - Promises, async functions, Promise.all/race/allSettled
- Classes - Inheritance, static blocks, private fields, getters/setters
- Generators - function*, yield, yield*, for...of iteration
- Destructuring - Arrays, objects, function parameters, rest/spread
- eval() - Dynamic code evaluation
- Built-ins - Array, String, Object, Map, Set, Date, RegExp, JSON, Math, Proxy, Reflect, Symbol
Embedding
- Minimal Runtime - Small footprint, no Node.js dependency
- Rust & C APIs - Full integration support for host applications
- WASM Support - Run in browsers via WebAssembly
- no_std Compatible - Can run in environments without the standard library
Installation
CLI
Library (Rust)
[]
= "0.1"
C/C++ Embedding
# Produces: target/release/libtsrun.so (Linux), .dylib (macOS), .dll (Windows)
Quick Start
CLI
# Run a TypeScript file
# With ES modules
Rust Library
use ;
C Embedding
int
Rust API
Basic Execution
use ;
let mut interp = new;
interp.prepare?;
loop
ES Module Loading
The interpreter uses step-based execution that pauses when imports are needed:
use ;
let mut interp = new;
// Main module with imports
interp.prepare?;
loop
Working with Values
use ;
use json;
let mut interp = new;
let guard = create_guard;
// Create values from JSON
let user = create_from_json?;
// Read properties
let name = get_property?;
assert_eq!;
// Modify properties
set_property?;
// Call methods
let tags = get_property?;
let joined = call_method?;
assert_eq!;
Async/Await with Orders
For async operations, the interpreter pauses with pending "orders" that the host fulfills:
use ;
let mut interp = new;
// Code that uses the order system for async I/O
interp.prepare?;
loop
Accessing Module Exports
use ;
let mut interp = new;
interp.prepare?;
// Run to completion
loop
// Access exports
let version = get_export;
assert_eq!;
let export_names = get_export_names;
assert!;
assert!;
C API
See examples/c-embedding/ for complete examples.
WASM / Browser
The interpreter compiles to WebAssembly for browser execution. Try it at the online playground.
Building
JavaScript API
import init from './pkg/tsrun.js';
await ;
const runner = ;
// Status constants (functions that return values)
const Status = ;
// Prepare and run
runner.;
while
Async Operations
TypeScript code can use order for async operations that the JavaScript host fulfills:
import { order } from "tsrun:host";
function fetch(url: string): Promise<any> {
return order({ type: "fetch", url });
}
const [user, posts] = await Promise.all([
fetch("/api/users/1"),
fetch("/api/posts")
]);
// In the step loop, handle STEP_SUSPENDED status:
if
Native Functions
Register C functions callable from JavaScript:
static TsRunValue*
// Register
TsRunValueResult fn = ;
;
// Use from JS: add(10, 20) -> 30
Module Loading
TsRunStepResult result = ;
while
Feature Flags
| Flag | Description | Default |
|---|---|---|
std |
Full standard library support | Yes |
regex |
Regular expression support (requires std) |
Yes |
console |
Console.log builtin | Yes |
c-api |
C FFI for embedding (requires std) |
No |
# Minimal build without regex
[]
= { = "0.1", = false, = ["std"] }
# With C API
[]
= { = "0.1", = ["c-api"] }
TypeScript Examples
Enums
enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
enum Status {
Pending = "pending",
Active = "active",
Completed = "completed"
}
interface Config {
logLevel: LogLevel;
status: Status;
retries: number;
}
const config: Config = {
logLevel: LogLevel.Info,
status: Status.Active,
retries: 3
};
// Reverse mapping works for numeric enums
LogLevel[1]; // "Info"
Classes with Inheritance
class Entity {
static #count = 0;
#id: number;
constructor() {
this.#id = ++Entity.#count;
}
get id() { return this.#id; }
static getCount() { return Entity.#count; }
}
class User extends Entity {
constructor(public name: string, public email: string) {
super();
}
toJSON() {
return { id: this.id, name: this.name, email: this.email };
}
}
const user = new User("Alice", "alice@example.com");
JSON.stringify(user.toJSON());
Generators
function* fibonacci(): Generator<number> {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
function* take<T>(gen: Generator<T>, n: number): Generator<T> {
for (const value of gen) {
if (n-- <= 0) return;
yield value;
}
}
const first10 = [...take(fibonacci(), 10)];
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Async/Await
async function fetchUserWithPosts(userId: number) {
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchUserPosts(userId)
]);
return { user, posts };
}
async function processAll<T, R>(
items: T[],
fn: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = [];
for (const item of items) {
results.push(await fn(item));
}
return results;
}
Config Generation
import { DEFAULT_CONFIG } from "./defaults";
const envOverrides = {
production: {
database: { host: "prod-db.example.com", ssl: true },
logging: { level: "warn" }
},
development: {
database: { host: "localhost" },
logging: { level: "debug" }
}
};
function buildConfig(env: string) {
return {
...DEFAULT_CONFIG,
...(envOverrides[env] || {})
};
}
JSON.stringify(buildConfig("production"), null, 2);
Testing
# Run all tests
# Run specific test
# Run with output
Test262 Conformance
Performance
The interpreter uses a register-based bytecode VM:
- Fewer instructions than stack-based VMs
- Better cache locality
- Efficient state capture for async/generators
Release builds use LTO and are optimized for size (opt-level = "z").
Limitations
- No runtime type checking - Types are parsed and stripped for IDE support, not validated at runtime
- Strict mode only - All code runs in strict mode
- Single-threaded - One interpreter instance per thread
License
MIT