Tagged Template Macros
Tagged template macros process template literals at compile time.
When to Use
- Create DSLs:
sql`SELECT ...` - Validate at compile time:
regex`[a-z]+` - Transform templates:
html`<div>...</div>` - Embed external content
Basic Structure
typescript
import { defineTaggedTemplateMacro } from "@typesugar/core";
import * as ts from "typescript";
defineTaggedTemplateMacro("myTag", {
expand(ctx, node: ts.TaggedTemplateExpression): ts.Expression {
const template = node.template;
// Handle the template...
return ctx.createStringLiteral("result");
},
});
// Runtime placeholder
export function myTag(strings: TemplateStringsArray, ...values: unknown[]): string {
throw new Error("myTag should be compiled away");
}Template Structure
A tagged template has two cases:
No Substitutions
typescript
tag`hello world`;
// template is NoSubstitutionTemplateLiteral
// template.text = "hello world"With Substitutions
typescript
tag`hello ${name}!`;
// template is TemplateExpression
// template.head.text = "hello "
// template.templateSpans[0].expression = name
// template.templateSpans[0].literal.text = "!"Tutorial: Creating upper
A tag that uppercases static strings at compile time:
typescript
import { defineTaggedTemplateMacro } from "@typesugar/core";
import * as ts from "typescript";
defineTaggedTemplateMacro("upper", {
expand(ctx, node) {
const template = node.template;
if (ts.isNoSubstitutionTemplateLiteral(template)) {
// Simple case: no interpolations
return ctx.createStringLiteral(template.text.toUpperCase());
}
// With interpolations: uppercase static parts, keep dynamic
const head = ctx.factory.createTemplateHead(template.head.text.toUpperCase());
const spans = template.templateSpans.map((span, i) => {
const isLast = i === template.templateSpans.length - 1;
const literal = isLast
? ctx.factory.createTemplateTail(span.literal.text.toUpperCase())
: ctx.factory.createTemplateMiddle(span.literal.text.toUpperCase());
return ctx.factory.createTemplateSpan(span.expression, literal);
});
return ctx.factory.createTemplateExpression(head, spans);
},
});Tutorial: Creating sql
Type-safe SQL with parameterized queries:
typescript
import { defineTaggedTemplateMacro } from "@typesugar/core";
import { quote } from "@typesugar/core/quote";
import * as ts from "typescript";
defineTaggedTemplateMacro("sql", {
expand(ctx, node) {
const template = node.template;
if (ts.isNoSubstitutionTemplateLiteral(template)) {
return quote(ctx)`{
text: ${ctx.createStringLiteral(template.text)},
params: []
}`;
}
// Build parameterized query
let text = template.head.text;
const params: ts.Expression[] = [];
template.templateSpans.forEach((span, i) => {
params.push(span.expression);
text += `$${i + 1}` + span.literal.text;
});
return quote(ctx)`{
text: ${ctx.createStringLiteral(text)},
params: [${params.map((p) => p).join(", ")}]
}`;
},
});Compile-Time Validation
Validate templates at build time:
typescript
defineTaggedTemplateMacro("regex", {
expand(ctx, node) {
const template = node.template;
if (!ts.isNoSubstitutionTemplateLiteral(template)) {
ctx.reportError(node, "regex tag does not support interpolations");
return node;
}
const pattern = template.text;
// Validate the regex
try {
new RegExp(pattern);
} catch (e) {
ctx.reportError(node, `Invalid regex: ${e}`);
return node;
}
return quote(ctx)`new RegExp(${ctx.createStringLiteral(pattern)})`;
},
});Accessing Tag Arguments
Some tagged templates have type arguments:
typescript
tag<Type>`template`;
// node.typeArguments contains [Type]typescript
defineTaggedTemplateMacro("typed", {
expand(ctx, node) {
const typeArgs = node.typeArguments;
if (typeArgs && typeArgs.length > 0) {
const firstType = typeArgs[0];
// Use the type...
}
return ctx.createStringLiteral("result");
},
});Returning Complex Types
Generate typed objects:
typescript
defineTaggedTemplateMacro("config", {
expand(ctx, node) {
const template = node.template;
if (!ts.isNoSubstitutionTemplateLiteral(template)) {
return node;
}
// Parse YAML-like config at compile time
const text = template.text;
const lines = text.trim().split("\n");
const entries: string[] = [];
for (const line of lines) {
const [key, value] = line.split(":").map((s) => s.trim());
if (key && value) {
entries.push(`${key}: ${JSON.stringify(value)}`);
}
}
return ctx.parseExpression(`({ ${entries.join(", ")} })`);
},
});Handling Raw Strings
Access raw (unescaped) text:
typescript
defineTaggedTemplateMacro("raw", {
expand(ctx, node) {
const template = node.template;
if (ts.isNoSubstitutionTemplateLiteral(template)) {
// template.rawText is the unescaped version
return ctx.createStringLiteral(template.rawText ?? template.text);
}
// Handle template expression...
return node;
},
});Error Reporting
Provide helpful compile-time errors:
typescript
defineTaggedTemplateMacro("json", {
expand(ctx, node) {
const template = node.template;
if (!ts.isNoSubstitutionTemplateLiteral(template)) {
ctx.reportError(node, "json tag requires a static template");
return node;
}
try {
const parsed = JSON.parse(template.text);
return ctx.parseExpression(JSON.stringify(parsed));
} catch (e) {
ctx.reportError(node, `Invalid JSON: ${e}`);
return node;
}
},
});Testing Tagged Templates
typescript
import { expandCode } from "@typesugar/testing";
describe("sql tag", () => {
it("extracts parameters", async () => {
const result = await expandCode(`
import { sql } from "./sql";
const id = 42;
const query = sql\`SELECT * FROM users WHERE id = \${id}\`;
`);
expect(result.code).toContain('text: "SELECT * FROM users WHERE id = $1"');
expect(result.code).toContain("params: [id]");
});
it("handles no interpolations", async () => {
const result = await expandCode(`
import { sql } from "./sql";
const query = sql\`SELECT * FROM users\`;
`);
expect(result.code).toContain("params: []");
});
});Best Practices
- Handle both template types: NoSubstitutionTemplateLiteral and TemplateExpression
- Validate early: Catch errors at compile time
- Preserve source info: Use proper span positions for errors
- Type the output: Generate well-typed code
- Document syntax: Explain what the tag expects
Next Steps
- Expression Macros — Function call macros
- Testing Macros — Verifying macro output
