-
Notifications
You must be signed in to change notification settings - Fork 2
Utils VariableReplacer
One-shot ${var} template substitution for dynamic templates
(the template string isn't known until runtime).
variableReplacer(message, context) is exactly
templatize(message, { onMissing: 'literal' })(context) rolled into a
single call. It exists for the use case where the template comes from
runtime input — error messages built per-throw, config files with ${}
substitution, log handler path templates, etc.
For static templates known at module-load time, use
templatize directly and reuse the returned
renderer — you'll pay the compile cost once instead of once per call.
-
Variable substitution: replaces
${key}placeholders. -
Dot-path lookup:
${user.name}walkscontext.user.name. -
Array formatting: arrays render as
(a, b, c). -
Type coercion: numbers, booleans →
String(value),null→'null'. -
Safe on missing: unknown placeholders keep the
${name}text (theonMissing: 'literal'contract). -
Circular references: surfaced via
JSON.stringifywhen a substituted plain-object value is part of a cycle.
deno add @tundralibs/utilsvariableReplacer(
message: string,
context: Record<string, unknown>,
regex?: RegExp,
): stringParameters
-
message— template string with placeholders. -
context— source values (object trees walked recursively via dot paths). -
regex— optional custom placeholder pattern. Must beg-flagged and have exactly one capture group around the variable name. Defaults to the${...}form handled bytemplatize.
Returns — message with placeholders substituted.
Throws — TypeError when a substituted plain-object value
participates in a circular reference graph (raised by JSON.stringify
during value-to-string conversion).
import { variableReplacer } from '@tundralibs/utils';
variableReplacer('Hello ${name}!', { name: 'World' });
// 'Hello World!'import { variableReplacer } from '@tundralibs/utils';
variableReplacer(
'User: ${user.firstName} ${user.lastName} (${user.id})',
{ user: { firstName: 'John', lastName: 'Doe', id: 123 } },
);
// 'User: John Doe (123)'import { variableReplacer } from '@tundralibs/utils';
variableReplacer('Available: ${colors}', { colors: ['red', 'green', 'blue'] });
// 'Available: (red, green, blue)'import { variableReplacer } from '@tundralibs/utils';
variableReplacer('Name: ${name}, City: ${city}', { name: 'Bob' });
// 'Name: Bob, City: ${city}' ← unmapped placeholder survivesWhen the template comes from a system that uses non-${} syntax
(handlebars-style {{...}}, shell-style $NAME, etc.), pass a
custom regex. It MUST be global (/g) and have exactly one capture
group around the variable name.
import { variableReplacer } from '@tundralibs/utils';
// Handlebars-style
variableReplacer(
'Hello {{name}}!',
{ name: 'World' },
/\{\{([^}]+)\}\}/g,
);
// 'Hello World!'
// Shell-style $NAME
variableReplacer(
'export PATH=$PATH:/usr/local/bin',
{ PATH: '/bin:/usr/bin' },
/\$([A-Z_][A-Z0-9_]*)/g,
);
// 'export PATH=/bin:/usr/bin:/usr/local/bin'The custom-regex path uses a hand-rolled scanner (not templatize),
but otherwise behaves identically: dot-path lookup, array formatting
as (a, b, c), missing-key keeps the original placeholder.
import { variableReplacer } from '@tundralibs/utils';
class ValidationError {
constructor(field: string, value: unknown, rule: string) {
const message = variableReplacer(
"Validation failed: '${field}' with value '${value}' must ${rule}",
{ field, value, rule },
);
throw new Error(message);
}
}This is how BaseError, EngineError, and GuardianError build
their messages from per-class templates.
| Situation | Use |
|---|---|
| Template is a string literal in source code |
templatize — type-checked, compile once |
| Template comes from config / user / runtime |
variableReplacer — one-shot |
| Template is reused but built at runtime |
templatize(message) at construction, hold the renderer |
Benched on Apple M2 / Deno 2.7.11 (packages/utils/variableReplacer.bench.ts):
| Scenario | Time |
|---|---|
| 3 vars from nested user object | ~820 ns |
| 2 vars with deeply nested context object | ~705 ns |
About 16–36% faster than the previous flatten-then-regex implementation, because the compile-once-then-render path is cheaper than walking the whole context tree per call.
For hot paths that reuse the same template (millions of calls),
pre-compile via templatize instead — the per-call cost drops to
~50–250 ns depending on variable count.
-
Missing keys keep the placeholder:
${name}stays as${name}. Picktemplatize(t, { onMissing: 'empty' })if you want them to vanish. -
nullrenders as the string'null'.undefinedis treated as missing. -
Arrays render as
(a, b, c). Nested objects in arrays default-stringify to'[object Object]'(legacy contract). -
Datevalues render as ISO 8601 strings (toISOString());RegExpand function values render viatoString(). -
Plain objects render via
JSON.stringify. Circular references in those objects throwTypeError.
- templatize — compile-once, render-many; the underlying engine.
-
Config — uses
variableReplacerfor env-var substitution in config file contents. - BaseError — uses it for per-instance error message templating.
-
Slogger
simpleFormatter— usestemplatizedirectly for the compile-once benefit.