macroforge_ts_quote
Warning: This is a work in progress and probably won't work for you. Use at your own risk!
Quote macro for generating TypeScript code at compile time.
Part of the macroforge project.
ts_template! - Rust-Style TypeScript Code Generation
The ts_template! macro provides an intuitive, template-based way to generate TypeScript code in your macros. It uses Rust-inspired syntax for control flow and interpolation.
Quick Reference
| Syntax | Description |
|---|---|
@{expr} |
Interpolate a Rust expression (always adds space after) |
{| content |} |
Ident block: concatenates content without spaces (e.g., {|get@{name}|}→getUser) |
@@{ |
Escape for literal @{ (e.g., "@@{foo}" → @{foo}) |
"text @{expr}" |
String interpolation (auto-detected) |
"'^template ${js}^'" |
JS backtick template literal (outputs `template ${js}`) |
{#if cond}...{/if} |
Conditional block |
{#if cond}...{:else}...{/if} |
Conditional with else |
{#if a}...{:else if b}...{:else}...{/if} |
Full if/else-if/else chain |
{#if let pattern = expr}...{/if} |
Pattern matching if-let |
{#match expr}{:case pattern}...{/match} |
Match expression with case arms |
{#for item in list}...{/for} |
Iterate over a collection |
{#while cond}...{/while} |
While loop |
{#while let pattern = expr}...{/while} |
While-let pattern matching loop |
{$let name = expr} |
Define a local constant |
{$let mut name = expr} |
Define a mutable local variable |
{$do expr} |
Execute a side-effectful expression |
{$typescript stream} |
Inject a TsStream, preserving its source and runtime_patches (imports) |
Note: A single
@not followed by{passes through unchanged (e.g.,email@domain.comworks as expected).
Syntax
Interpolation: @{expr}
Insert Rust expressions into the generated TypeScript:
let class_name = "User";
let method = "toString";
let code = ts_template! ;
Generates:
User.prototype.toString = function () {
return "User instance";
};
Identifier Concatenation: {| content |}
When you need to build identifiers dynamically (like getUser, setName), use the ident block syntax. Everything inside {| |} is concatenated without spaces:
let field_name = "User";
let code = ts_template!
};
Generates:
function getUser() {
return this.user;
}
Without ident blocks, @{} always adds a space after for readability. Use {| |} when you explicitly want concatenation:
let name = "Status";
// With space (default behavior)
ts_template! // → "namespace Status"
// Without space (ident block)
ts_template! } // → "namespaceStatus"
Multiple interpolations can be combined:
let entity = "user";
let action = "create";
ts_template! } // → "user_create"
String Interpolation: "text @{expr}"
Interpolation works automatically inside string literals - no format!() needed:
let name = "World";
let count = 42;
let code = ts_template! ;
Generates:
console.log("Hello World!");
console.log("Count: 42, doubled: 84");
This also works with method calls and complex expressions:
let field = "username";
let code = ts_template! ;
Backtick Template Literals: "'^...^'"
For JavaScript template literals (backtick strings), use the '^...^' syntax. This outputs actual backticks and passes through ${} for JS interpolation:
let tag_name = "div";
let code = ts_template! ;
Generates:
const html = `<div>${content}</div>`;
You can mix Rust @{} interpolation (evaluated at macro expansion time) with JS ${} interpolation (evaluated at runtime):
let class_name = "User";
let code = ts_template! ;
Generates:
`Hello ${this.name}, you are a User`;
Conditionals: {#if}...{:else}...{/if}
let needs_validation = true;
let code = ts_template! ;
With else:
let has_default = true;
let code = ts_template! ;
With else-if:
let level = 2;
let code = ts_template! ;
Pattern Matching: {#if let pattern = expr}...{/if}
Use if let for pattern matching on Option, Result, or other Rust enums:
let maybe_name: = Some;
let code = ts_template! ;
Generates:
console.log("Hello, Alice!");
This is useful when working with optional values from your IR:
let code = ts_template! ;
Match Expressions: {#match expr}{:case pattern}...{/match}
Use match for exhaustive pattern matching:
let visibility = Public;
let code = ts_template! ;
Generates:
public field: string;
Match with value extraction:
let result: = Ok;
let code = ts_template! ;
Match with wildcard:
let count = 5;
let code = ts_template! ;
Iteration: {#for item in list}...{/for}
let fields = vec!;
let code = ts_template! ;
Generates:
function toJSON() {
const result = {};
result.name = this.name;
result.email = this.email;
result.age = this.age;
return result;
}
Tuple Destructuring in Loops
let items = vec!;
let code = ts_template! ;
Local Constants: {$let name = expr}
Define local variables within the template scope:
let items = vec!;
let code = ts_template! ;
This is useful for computing derived values inside loops without cluttering the Rust code.
TsStream Injection: {$typescript stream}
Inject another TsStream into your template, preserving both its source code and runtime patches (like imports added via add_import()):
// Create a helper method with its own import
let mut helper = body! ;
helper.add_import;
// Inject the helper into the main template
let result = body! ;
// result now includes helper's source AND its Result import
This is essential for composing multiple macro outputs while preserving imports and patches:
let extra_methods = if include_validation else ;
body!
Complete Example: JSON Derive Macro
Before (manual AST building):
After (with ts_template!):
How It Works
- Compile-Time: The template is parsed during macro expansion
- String Building: Generates Rust code that builds a TypeScript string at runtime
- SWC Parsing: The generated string is parsed with SWC to produce a typed AST
- Result: Returns
Stmtthat can be used inMacroResultpatches
Return Type
ts_template! returns a Result<Stmt, TsSynError> by default. The macro automatically unwraps and provides helpful error messages showing the generated TypeScript code if parsing fails.
Nesting and Regular TypeScript
You can mix template syntax with regular TypeScript. Braces {} are recognized as either:
- Template tags if they start with
#,$,:, or/ - Regular TypeScript blocks otherwise
ts_template!
Advanced: Nested Iterations
let classes = vec!;
ts_template!
Comparison with Alternatives
| Approach | Pros | Cons |
|---|---|---|
ts_quote! |
Compile-time validation, type-safe | Can't handle Vec, verbose |
parse_ts_str() |
Maximum flexibility | Runtime parsing, less readable |
ts_template! |
Readable, handles loops/conditions | Small runtime parsing overhead |
Best Practices
- Use
ts_template!for complex code generation with loops/conditions - Use
ts_quote!for simple, static statements - Keep templates readable - extract complex logic into variables
- Don't nest templates too deeply - split into helper functions
Error Messages
If the generated TypeScript is invalid, you'll see:
Failed to parse generated TypeScript:
User.prototype.toJSON = function( {
return {};
}
This shows you exactly what was generated, making debugging easy!