Expand description
A minimal TypeScript runtime for embedding in applications.
This crate provides a TypeScript interpreter written in Rust, designed for configuration files where users benefit from IDE autocompletion, type checking, and error highlighting. TypeScript features like enums, interfaces, and generics are fully parsed; types are stripped at runtime (not type-checked).
§TypeScript Features
The interpreter supports TypeScript-specific syntax for better editor experience:
- Enums - Numeric and string enums with reverse mappings
- Interfaces & Types - Parsed for IDE support, stripped at runtime
- Decorators - Class, method, property, and parameter decorators
- Namespaces - TypeScript namespace declarations
- Generics - Generic functions and classes
- Parameter Properties -
constructor(public x: number)syntax
§Quick Start
use tsrun::{Interpreter, StepResult};
let mut interp = Interpreter::new();
interp.prepare(r#"
enum Status { Active = 1, Inactive = 0 }
interface Config { status: Status; }
const cfg: Config = { status: Status.Active };
cfg.status
"#, None).unwrap();
loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
assert_eq!(value.as_number(), Some(1.0));
break;
}
_ => panic!("Unexpected result"),
}
}§Execution Model
The interpreter uses step-based execution, giving hosts full control:
Interpreter::prepare- Compiles code and prepares for executionInterpreter::step- Executes one instruction, returnsStepResultStepResult::NeedImports- Execution paused, waiting for ES modulesStepResult::Suspended- Execution paused, waiting for async operations
§Working with Values
Use the api module for creating and manipulating JavaScript values:
use tsrun::{Interpreter, api};
let mut interp = Interpreter::new();
let guard = api::create_guard(&interp);
// Create objects from JSON
let user = api::create_from_json(&mut interp, &guard, &serde_json::json!({
"name": "Alice",
"scores": [95, 87, 92]
})).unwrap();
// Read properties
let name = api::get_property(&user, "name").unwrap();
assert_eq!(name.as_str(), Some("Alice"));
// Call methods on arrays
let scores = api::get_property(&user, "scores").unwrap();
let joined = api::call_method(&mut interp, &guard, &scores, "join", &["-".into()]).unwrap();
assert_eq!(joined.as_str(), Some("95-87-92"));§Module Loading
ES modules are loaded on-demand. When execution needs an import:
use tsrun::{Interpreter, StepResult, ModulePath};
let mut interp = Interpreter::new();
interp.prepare(r#"import { x } from "./config.ts"; x"#, Some("/main.ts".into())).unwrap();
loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::NeedImports(imports) => {
for import in imports {
// Host provides module source code
let source = "export const x = 42;";
interp.provide_module(import.resolved_path, source).unwrap();
}
}
StepResult::Complete(value) => {
assert_eq!(value.as_number(), Some(42.0));
break;
}
_ => break,
}
}§Internal Modules
Register Rust functions as importable modules:
use tsrun::{Interpreter, InterpreterConfig, InternalModule, JsValue, Guarded, JsError};
fn get_version(
_interp: &mut Interpreter,
_this: JsValue,
_args: &[JsValue]
) -> Result<Guarded, JsError> {
Ok(Guarded::unguarded(JsValue::from("1.0.0")))
}
let config = InterpreterConfig {
internal_modules: vec![
InternalModule::native("app:version")
.with_function("getVersion", get_version, 0)
.build(),
],
..Default::default()
};
let interp = Interpreter::with_config(config);
// Now code can: import { getVersion } from "app:version";§GC Safety
Objects are garbage-collected. Use Guard to keep them alive:
use tsrun::{Interpreter, api};
let mut interp = Interpreter::new();
let guard = api::create_guard(&interp);
// Objects allocated with guard stay alive until guard is dropped
let obj = api::create_object(&mut interp, &guard).unwrap();
api::set_property(&obj, "x", 42.into()).unwrap();
// guard dropped here - obj may be collected§Use Case Examples
§Kubernetes Deployment Configuration
Generate type-safe Kubernetes manifests with IDE autocompletion:
use tsrun::{Interpreter, StepResult, js_value_to_json};
let mut interp = Interpreter::new();
interp.prepare(r#"
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 })
"#, None).unwrap();
let result = loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
break js_value_to_json(value.value()).unwrap();
}
_ => panic!("Unexpected result"),
}
};
assert_eq!(result["apiVersion"], "apps/v1");
assert_eq!(result["kind"], "Deployment");
assert_eq!(result["metadata"]["name"], "api");
assert_eq!(result["spec"]["replicas"], 3);§Game Item Configuration
Define game items with enums and computed loot tables:
use tsrun::{Interpreter, StepResult, js_value_to_json};
let mut interp = Interpreter::new();
interp.prepare(r#"
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"] }
])
"#, None).unwrap();
let result = loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
break js_value_to_json(value.value()).unwrap();
}
_ => panic!("Unexpected result"),
}
};
// Common item: dropWeight=50, sellPrice=100*(1+0*0.5)=100
assert_eq!(result[0]["name"], "Iron Sword");
assert_eq!(result[0]["dropWeight"], 50);
assert_eq!(result[0]["sellPrice"], 100);
// Legendary item: dropWeight=1, sellPrice=5000*(1+3*0.5)=12500
assert_eq!(result[1]["name"], "Dragon Scale");
assert_eq!(result[1]["dropWeight"], 1);
assert_eq!(result[1]["sellPrice"], 12500);
assert_eq!(result[1]["effects"][0], "Fire Resistance");§API Router Configuration
Configure REST endpoints with typed middleware and rate limits:
use tsrun::{Interpreter, StepResult, js_value_to_json};
let mut interp = Interpreter::new();
interp.prepare(r#"
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
"#, None).unwrap();
let result = loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
break js_value_to_json(value.value()).unwrap();
}
_ => panic!("Unexpected result"),
}
};
assert_eq!(result.as_array().unwrap().len(), 3);
assert_eq!(result[0]["method"], "GET");
assert_eq!(result[0]["path"], "/users/:id");
assert_eq!(result[1]["rateLimit"]["requests"], 10);
assert_eq!(result[2]["middleware"][1], "admin");§Build Tool Configuration
Create plugin-based build configurations like webpack or vite:
use tsrun::{Interpreter, StepResult, js_value_to_json};
let mut interp = Interpreter::new();
interp.prepare(r#"
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
"#, None).unwrap();
let result = loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
break js_value_to_json(value.value()).unwrap();
}
_ => panic!("Unexpected result"),
}
};
assert_eq!(result["entry"], "./src/index.ts");
assert_eq!(result["output"]["path"], "./dist");
assert_eq!(result["plugins"].as_array().unwrap().len(), 3);
assert_eq!(result["plugins"][0]["name"], "typescript");
assert_eq!(result["plugins"][0]["options"]["target"], "ES2022");
assert_eq!(result["minify"], true);§Validation Schema
Define form validation schemas with discriminated unions:
use tsrun::{Interpreter, StepResult, js_value_to_json};
let mut interp = Interpreter::new();
interp.prepare(r#"
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
"#, None).unwrap();
let result = loop {
match interp.step().unwrap() {
StepResult::Continue => continue,
StepResult::Complete(value) => {
break js_value_to_json(value.value()).unwrap();
}
_ => panic!("Unexpected result"),
}
};
assert_eq!(result.as_array().unwrap().len(), 2);
assert_eq!(result[0]["name"], "email");
assert_eq!(result[0]["label"], "Email Address");
assert_eq!(result[0]["rules"][0]["type"], "required");
assert_eq!(result[1]["rules"][1]["type"], "minLength");
assert_eq!(result[1]["rules"][1]["value"], 8);
assert_eq!(result[1]["rules"][2]["message"], "Must contain uppercase");Re-exports§
pub use error::JsError;pub use gc::Gc;pub use gc::GcStats;pub use gc::Guard;pub use gc::Heap;pub use gc::Reset;pub use string_dict::StringDict;pub use value::CheapClone;pub use value::EnvRef;pub use value::Guarded;pub use value::JsObject;pub use value::JsString;pub use value::JsValue;
Modules§
- api
- Public API for interacting with JavaScript values from Rust.
- ast
- Abstract Syntax Tree types for TypeScript
- compiler
- Bytecode compiler for TypeScript/JavaScript
- error
- Error types for the TypeScript interpreter.
- gc
- Mark-and-sweep garbage collection system.
- parser
- Generated parser for TypeScript.
- platform
- Platform abstraction traits for no_std compatibility.
- string_
dict - String dictionary for deduplicating JsString instances.
- value
- JavaScript value representation.
Structs§
- Import
Request - A pending import request with context about where it was requested from.
- Internal
Module - Definition of an internal module that can be imported from JavaScript.
- Interpreter
- The interpreter state
- Interpreter
Config - Configuration for creating an Interpreter
- Module
Path - A normalized, absolute module path.
- Native
Module Builder - Builder for creating native internal modules
- Order
- An order is a request for an external effect. The payload is a RuntimeValue that the host interprets to perform side effects. The RuntimeValue keeps the payload alive until the order is fulfilled or dropped.
- OrderId
- Unique identifier for an order
- Order
Response - Response to fulfill an order from the host
- Runtime
Value - A JS value with an attached guard that keeps it alive until dropped.
Enums§
- Internal
Export - Definition of an export from an internal module
- Internal
Module Kind - How an internal module is defined
- Step
Result - Result of executing a single step.
Functions§
- create_
eval_ internal_ module - Create the tsrun:host module
- js_
value_ to_ json - Convert a JsValue to JSON, with public API for external callers (without circular detection)
- json_
to_ js_ value_ with_ guard - Convert a serde_json value to a JsValue using a provided guard. The guard keeps any created objects alive until it is dropped. This is the preferred method when you need to control the lifetime of the result.
- json_
to_ js_ value_ with_ interp - Convert a serde_json value to a JsValue using the interpreter’s GC space
Type Aliases§
- Internal
Fn - A native function that can be exported from an internal module