-
Notifications
You must be signed in to change notification settings - Fork 2
Utils Templatize
Type-safe ${var} template compiler. Compiles once, renders many.
templatize is the canonical template engine for the project. Slogger's
simpleFormatter, BaseError's message templates, and any other place
that needs ${var}-style substitution all flow through it.
- Compile once, render many: parse the template into literal + lookup tokens at construction; render is a tight loop, no regex per call.
-
Type-safe: variable names are extracted at compile time from a
template string literal, so misspelled or extra keys are TypeScript
errors rather than silent failures at runtime. The keys themselves
are optional — omitting one is legal, and
onMissingdecides how it renders. -
Dot-path lookup:
${user.name}walksvalues.user.nameand accepts the flat{'user.name': 'x'}form. -
Arrays render as
(a, b, c). Plain objects render viaJSON.stringify. Dates render as ISO 8601 strings (toISOString()); RegExp and function values render viatoString(). -
Configurable missing-key behaviour:
'empty'(default) emits'';'literal'keeps the${var}text. Pick the right one for your destination.
deno add @tundralibs/utilstemplatize<T extends string>(
template: T,
options?: TemplateOptions,
): (values: TemplateValues<T>) => string
type TemplateOptions = {
onMissing?: 'empty' | 'literal'; // default: 'empty'
};TemplateValues<T> is computed at the type level from the template
string — one optional key is inferred per ${name} placeholder. Keys
are optional because the renderer explicitly handles absent values via
onMissing; keys that aren't placeholders are still rejected.
import { templatize } from '@tundralibs/utils';
const greet = templatize('Hello, ${name}! Welcome to ${place}.');
greet({ name: 'Alice', place: 'TypeScript' });
// 'Hello, Alice! Welcome to TypeScript.'
// Omitting a placeholder is allowed — `onMissing` decides the output.
greet({ name: 'Bob' }); // 'Hello, Bob! Welcome to .'
greet({
name: 'Bob',
// @ts-expect-error extra key: not a placeholder in the template
location: 'Somewhere',
});For human-tailed output (logs, debug prints), unmapped variables should stay visible:
import { templatize } from '@tundralibs/utils';
const line = templatize('[${time}] ${level}: ${msg}', { onMissing: 'literal' });
line({ time: '12:00:01', msg: 'hi' }); // `level` simply omitted
// '[12:00:01] ${level}: hi' ← the `${level}` placeholder survivesThis is what slogger's simpleFormatter uses under the hood.
For URLs / SQL / messages that go to users or services, missing
fields should disappear, not leak ${...} syntax:
import { templatize } from '@tundralibs/utils';
const url = templatize('/users/${id}?token=${token}');
url({ id: '42' }); // `token` simply omitted
// '/users/42?token=' ← clean empty rather than `?token=${token}`import { templatize } from '@tundralibs/utils';
const fmt = templatize('User: ${user.name} <${user.email}>');
// Both shapes work at runtime:
fmt({ 'user.name': 'Alice', 'user.email': 'a@x.com' }); // flat
fmt({ user: { name: 'Alice', email: 'a@x.com' } } as any); // nested
// Both → 'User: Alice <a@x.com>'(Flat keys win over nested when both are present, for back-compat.)
import { templatize } from '@tundralibs/utils';
const fmt = templatize('Tags: ${tags}');
fmt({ tags: ['ts', 'logger', 'fast'] as unknown as string });
// 'Tags: (ts, logger, fast)'- A template with no placeholders compiles to a constant function:
import { templatize } from '@tundralibs/utils'; const c = templatize('Static text'); c(null as any); // 'Static text' — no values needed
- A template that is just one
${...}skips the per-token loop.
Benched on Apple M2 / Deno 2.7.11 (packages/utils/templatize.bench.ts):
| Operation | Time |
|---|---|
| compile, 2 vars | ~88 ns |
| compile, 10 vars | ~320 ns |
| compile, all-literal | ~46 ns |
| render, 2 vars (pre-compiled) | ~50 ns |
| render, 10 vars (pre-compiled) | ~255 ns |
| render, dot-path on nested | ~200 ns |
| render, all-literal (constant fn) | ~4 ns |
The expected idiom is compile at module scope, render in hot loops.
A one-shot wrapper that compiles per call is provided as
variableReplacer — use it when the
template string itself is dynamic (loaded from config, error message
construction, etc.).
Matches the legacy variableReplacer contract so the two can be
swapped freely:
| Value type | Rendered as |
|---|---|
string |
the string |
number, boolean, bigint
|
String(value) |
null |
'null' |
undefined / missing |
per onMissing option |
| array | '(a, b, c)' |
| plain object | JSON.stringify(value) |
Date |
value.toISOString() (ISO 8601) |
RegExp |
value.toString() |
function |
value.toString() (source form) |
| Feature | templatize |
variableReplacer |
|---|---|---|
| Compile-time type safety | ✅ via TemplateValues<T>
|
❌ runtime only |
| Compile cost paid | once at construction | every call |
| Best for | static templates | dynamic templates |
| Dot-path / nested lookup | ✅ | ✅ |
| Missing-key behaviour |
'empty' or 'literal'
|
always 'literal'
|
| Arrays / objects / null | identical to variableReplacer
|
reference contract |
-
variableReplacer — one-shot wrapper
around
templatizefor dynamic templates. -
Slogger
simpleFormatter— usestemplatizewithonMissing: 'literal'. - BaseError — uses the template engine for contextualised error messages.