npm package: @t09tanaka/typed-hbs
Description: Minimal typed Handlebars-style template engine for TypeScript
Author: t09tanaka
Latest version: 0.1.1

# typed-hbs

Minimal typed Handlebars-style template engine for TypeScript.

`{{key}}` placeholders are replaced with values from a typed context object. Zero dependencies.

## Install

```bash
npm install @t09tanaka/typed-hbs
```

## Usage

```ts
import { render } from "@t09tanaka/typed-hbs";

type ContactMailProps = {
  name: string;
  email: string;
  meta: { ip: string };
};

const props: ContactMailProps = {
  name: "Alice",
  email: "alice@example.com",
  meta: { ip: "127.0.0.1" },
};

render<ContactMailProps>("Hello {{name}}", props);
// => "Hello Alice"

render<ContactMailProps>("ip={{ meta.ip }}", props);
// => "ip=127.0.0.1"
```

### Dot paths

Nested objects are accessed with `.` separated paths.

```ts
render("{{a.b.c}}", { a: { b: { c: "deep" } } });
// => "deep"
```

### Array indices

Array elements are accessed by numeric index.

```ts
render("{{items.0.title}}", { items: [{ title: "First" }] });
// => "First"
```

### Loops

Iterate over arrays with `{{#each}}`. Parent props are accessible inside the loop, and item properties take precedence.

```ts
const template = "{{#each items}}- {{title}} by {{author}}\n{{/each}}";

const props = {
  author: "Alice",
  items: [{ title: "Post 1" }, { title: "Post 2" }],
};

render(template, props);
// => "- Post 1 by Alice\n- Post 2 by Alice\n"
//
// - Post 1 by Alice
// - Post 2 by Alice
```

For primitive arrays, use `{{.}}` to reference the current value:

```ts
const tagList = "{{#each tags}}#{{.}} {{/each}}";

render(tagList, { tags: ["ts", "js", "node"] });
// => "#ts #js #node "
```

### Conditionals

Show or hide content based on truthy/falsy values:

```ts
const tpl = `{{#if premium}}
  Premium member
{{else}}
  Free member
{{/if}}`;

render(tpl, { premium: true });
// =>
//   Premium member
//

render(tpl, { premium: false });
// =>
//   Free member
//
```

Chained conditions with `{{else if}}`:

```ts
const tpl = `{{#if role}}Admin{{else if guest}}Guest{{else}}User{{/if}}`;

render(tpl, { role: false, guest: true });
// => "Guest"
```

Use `{{#unless}}` for negated conditions:

```ts
const tpl = "{{#unless paid}}Please pay{{/unless}}";

render(tpl, { paid: false });
// => "Please pay"
```

## Behavior

| Value         | Result                                      |
| ------------- | ------------------------------------------- |
| `string`, etc | `String(value)`                             |
| `null`        | `""` (empty string)                         |
| `undefined`   | Throws `Error`                              |
| Missing key   | Throws `Error`                              |

- Prototype chain values are **not** resolved (`hasOwnProperty` check).
- HTML escaping is **not** performed. Escape on the caller side if needed.
- Empty arrays produce an empty string (the `{{#each}}` block is removed).
- Non-array values in `{{#each}}` throw an `Error`.
- `{{#if}}` uses JavaScript truthy/falsy rules (`false`, `0`, `""`, `null`, `undefined` are falsy).
- Blocks can be nested (`{{#if}}` inside `{{#each}}`, etc.).

## API

### `render<T>(template: string, props: T): string`

Replaces all `{{key}}` placeholders in `template` with values from `props`.

Throws an `Error` if any key is missing or resolves to `undefined`.

## Scripts

```bash
npm test          # Run tests
npm run build     # Build ESM + CJS
npm run typecheck # Type check
```

## License

MIT