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
Note: Types are parsed for IDE support but not checked at runtime. Type annotations, interfaces, and generics are stripped during execution. Use your editor's TypeScript language server for type checking.
- 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, Node.js, Go (wazero), and other WASM runtimes
- 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. The host examines the order payload to determine what operation to perform:
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
The interpreter compiles to WebAssembly with a C-style FFI, enabling use across multiple runtimes: browsers, Node.js, Go (via wazero), and others.
Building
Go Embedding (wazero)
import "github.com/example/tsrun-go/tsrun"
ctx := context.Background()
rt, _ := tsrun.New(ctx, tsrun.ConsoleOption(func(level tsrun.ConsoleLevel, msg string) ))
defer rt.Close(ctx)
interp, _ := rt.NewContext(ctx)
defer interp.Free(ctx)
interp.Prepare(ctx, `console.log("Hello from Go!")`, "/main.ts")
result, _ := interp.Run(ctx)
See examples/go-wazero/ for complete examples including async operations, modules, and native functions.
Browser/Node.js 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 |
wasm |
WebAssembly target support | No |
# Minimal build without regex
[]
= { = "0.1", = false, = ["std"] }
# With C API
[]
= { = "0.1", = ["c-api"] }
# Build for WASM
Use Case Examples
Note: The type annotations in these examples provide IDE autocompletion and editor-based type checking, but tsrun does not validate types at runtime. Passing a wrong type will not throw an error - it will simply execute with whatever value is provided.
Kubernetes Deployment Configuration
Generate type-safe Kubernetes manifests with IDE autocompletion:
interface DeploymentConfig {
name: string;
image: string;
replicas: number;
port: number;
}
function deployment(config: DeploymentConfig) {
return {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: { name: config.name },
spec: {
replicas: config.replicas,
selector: { matchLabels: { app: config.name } },
template: {
metadata: { labels: { app: config.name } },
spec: {
containers: [{
name: config.name,
image: config.image,
ports: [{ containerPort: config.port }]
}]
}
}
}
};
}
deployment({ name: "api", image: "myapp:v1.2.0", replicas: 3, port: 8080 })
Game Item Configuration
Define game items with enums and computed loot tables:
enum Rarity { Common, Rare, Epic, Legendary }
interface Item {
name: string;
rarity: Rarity;
basePrice: number;
effects?: string[];
}
function createLootTable(items: Item[]) {
return items.map(item => ({
...item,
dropWeight: item.rarity === Rarity.Legendary ? 1 :
item.rarity === Rarity.Epic ? 5 :
item.rarity === Rarity.Rare ? 15 : 50,
sellPrice: Math.floor(item.basePrice * (1 + item.rarity * 0.5))
}));
}
createLootTable([
{ name: "Iron Sword", rarity: Rarity.Common, basePrice: 100 },
{ name: "Dragon Scale", rarity: Rarity.Legendary, basePrice: 5000,
effects: ["Fire Resistance", "+50 Defense"] }
])
// Result: [{ dropWeight: 50, sellPrice: 100, ... }, { dropWeight: 1, sellPrice: 12500, ... }]
API Router Configuration
Configure REST endpoints with typed middleware and rate limits:
interface Route {
method: "GET" | "POST" | "PUT" | "DELETE";
path: string;
handler: string;
middleware?: string[];
rateLimit?: { requests: number; window: string };
}
const routes: Route[] = [
{
method: "GET",
path: "/users/:id",
handler: "users::get",
middleware: ["auth", "cache"]
},
{
method: "POST",
path: "/users",
handler: "users::create",
middleware: ["auth", "validate"],
rateLimit: { requests: 10, window: "1m" }
},
{
method: "DELETE",
path: "/users/:id",
handler: "users::delete",
middleware: ["auth", "admin"]
}
];
routes
Build Tool Configuration
Create plugin-based build configurations like webpack or vite:
interface Plugin {
name: string;
options?: Record<string, any>;
}
interface BuildConfig {
entry: string;
output: { path: string; filename: string };
plugins: Plugin[];
minify: boolean;
}
const config: BuildConfig = {
entry: "./src/index.ts",
output: {
path: "./dist",
filename: "[name].[hash].js"
},
plugins: [
{ name: "typescript", options: { target: "ES2022" } },
{ name: "minify", options: { dropConsole: true } },
{ name: "bundle-analyzer" }
],
minify: true
};
config
Validation Schema
Define form validation schemas with discriminated unions:
type Rule =
| { type: "required" }
| { type: "minLength"; value: number }
| { type: "maxLength"; value: number }
| { type: "pattern"; regex: string; message: string }
| { type: "email" };
interface FieldSchema {
name: string;
label: string;
rules: Rule[];
}
const userSchema: FieldSchema[] = [
{
name: "email",
label: "Email Address",
rules: [
{ type: "required" },
{ type: "email" }
]
},
{
name: "password",
label: "Password",
rules: [
{ type: "required" },
{ type: "minLength", value: 8 },
{ type: "pattern", regex: "[A-Z]", message: "Must contain uppercase" }
]
}
];
userSchema
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