Typed Handlebars
Goldar templates keep HTML visible while giving their inputs a TypeScript contract. In this tutorial, you will declare template props, render values safely, branch and iterate with checked expressions, compose templates, and connect the result to TypeScript.
Declare the template contract
Create src/user-card.hbs with a leading TypeScript frontmatter block:
---
type Props = {
readonly user: {
readonly name: string;
readonly role: string;
};
};
---
<article>
<h2>{{user.name}}</h2>
<p>{{user.role}}</p>
</article>
Both --- delimiters must occupy their own line, and the contract must be a Props type alias.
Goldar compiles the file into a callable module whose argument matches that type.
import UserCardView from "./user-card.hbs";
const card = UserCardView({
user: { name: "Trini", role: "Designer" },
});
After synchronization, TypeScript reports a missing role, an extra property, or the wrong value
type at this call site.
Use checked expressions
Template expressions read root props and their properties:
<p>{{user.profile.displayName}}</p>
Goldar escapes ordinary text and attribute expressions. Triple-mustache rendering accepts only a
TrustedHtml value created at an explicitly reviewed trust boundary; it is not a shortcut around
escaping for normal strings.
The current language is intentionally smaller than general Handlebars. Prefer direct property access and move calculations or application decisions into TypeScript rather than inventing template helpers.
Branch with if
Use if and else for presentation states:
{{#if user.isAdmin}}
<p>Administrator</p>
{{else}}
<p>Member</p>
{{/if}}
The expression is checked against Props, and the compiler preserves the branch structure in the
render program. unless, subexpressions, and general helper calls are not part of the implemented
subset.
Iterate with an explicit binding
Iteration introduces a lexical name instead of silently changing the meaning of this:
{{#if users.length}}
<ul>
{{#each users as user}}
<li>{{user.name}}</li>
{{/each}}
</ul>
{{else}}
<p>No users yet.</p>
{{/if}}
The required as user clause makes the item type visible to the compiler, editor tooling, and the
reader. The loop may include an else branch. Implicit this, parent traversal such as ../,
indices such as @index, and destructuring are not currently supported.
Compose typed templates
Make the card reusable by importing it from a parent template. Create src/user-list.hbs:
---
import type { User } from "./index.js";
import UserCardView from "./user-card.hbs";
UserCardView.as("user-card");
type Props = {
readonly users: readonly User[];
};
---
{{#if users.length}}
<div class="user-list">
{{#each users as user}}
<user-card user={{user}} />
{{/each}}
</div>
{{else}}
<p>No users yet.</p>
{{/if}}
UserCardView.as("user-card") gives the import a template-local tag name. This is compile-only
frontmatter syntax: Goldar retains the imported template for exact prop checks and removes the
.as() declaration from generated TypeScript. There is no global partial registry and no
string-based dynamic lookup.
The child call is checked against user-card.hbs’s Props. Missing, unknown, and incorrectly typed
props are compiler diagnostics at the call site.
Pass views through a layout
Templates can also accept ordered child views. A layout renders them at one {{outlet}}:
---
type Props = {
readonly title: string;
};
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{title}}</title>
</head>
<body>
<main>{{outlet}}</main>
</body>
</html>
Call the page and pass it to the layout from the route:
export interface User {
readonly name: string;
readonly role: string;
}
class UsersRoute extends Route {
render() {
const users: readonly User[] = [
{ name: "Trini", role: "Designer" },
{ name: "Billy", role: "Engineer" },
];
const page = UserListView({ users });
return LayoutView({ title: "Team" }, page);
}
}
An outlet does not add a wrapper element. A template may declare at most one outlet.
Keep styles near their markup
A normal style block is compiled with template-local selector scoping:
<style>
.user-list {
display: grid;
gap: 1rem;
}
</style>
Use <style is:global> when the whole block is intentionally global, or :global(...) around one
selector that must escape the local scope. Goldar collects compiled template styles into the
generated client stylesheet.
Synchronize and type-check
Goldar has two complementary type-safety layers:
npx goldar sync
npx tsc -p tsconfig.json
goldar sync generates exact .hbs module declarations beneath .goldar/types. TypeScript then
checks calls from .ts files into those template modules. tsc does not parse the expressions
inside Handlebars files.
Inside a template, the Typed Handlebars language service supplies completion, hover, definition
navigation, and diagnostics by mapping the template through a generated TypeScript shadow
document. Editor clients can run the typed-handlebars-language-server binary and provide an
element registry. See the Typed Handlebars API for that tooling
contract.
Describe custom-element attributes
An HTMLElement class does not tell TypeScript which HTML attributes its tag accepts. Language
tooling can read an explicit registry:
export interface ElementAttributes {
readonly "user-avatar": {
readonly "display-name": string;
readonly size?: "small" | "large";
};
}
With that registry, the language service can complete the tag and its attributes, explain them on hover, and diagnose missing, unknown, or invalid values:
<user-avatar display-name={{user.name}} size="large"></user-avatar>
Standard HTML elements remain permissive. The registry is for editor and tooling feedback; the custom element module still owns its browser runtime behavior.
Know the current boundary
Typed Handlebars currently supports root property paths, typed if, explicit each ... as
bindings, imported-template props, custom-element contracts, and source mappings that preserve
UTF-16 positions. It does not claim full Handlebars compatibility.
For a complete application using these patterns, read the todo-list example. Use the compiler reference for synchronization and build behavior.