Skip to content
JavaScript Basics: Process Template Literals with Tags

JavaScript Basics: Process Template Literals with Tags

October 22, 2026

A tagged template passes its literal text and interpolated values to a function separately. The function decides how to combine them and can return something other than a string. This is useful for structured logging, context-specific formatting, and raw text where backslashes need to remain visible.

These three independent examples were tested on Linux with Node.js v24.19.0. They use only built-in features, make no network requests, and include assertions. The exact files require Node.js 15.0.0 or later because they import node:assert/strict; the submodule was introduced in Node.js 15. That historical minimum is not a recommendation to use an obsolete release: use a currently supported Node.js version. Only the installed version above was executed for this tutorial.

Prepare a scratch directory

You need Bash, a text editor, Node.js on your PATH, and the mktemp, rm, and rmdir utilities. Keep the same terminal open. Save each named source file as UTF-8 in this directory; .mjs selects an ES module. The guarded commands stop if creating or entering the scratch directory fails.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Keep log values separate from literal text

Save this as capture-structured-log.mjs:

import assert from 'node:assert/strict';

function record(strings, ...values) {
  return { parts: [...strings], values };
}
const item = record`Exported ${3} files for ${'Ada'}.`;
assert.deepEqual(item.parts, ['Exported ', ' files for ', '.']);
assert.deepEqual(item.values, [3, 'Ada']);
assert.equal(typeof item.values[0], 'number');
assert.deepEqual(record`Ready`, { parts: ['Ready'], values: [] });
console.log(JSON.stringify(item));

Run it:

node capture-structured-log.mjs

Output:

{"parts":["Exported "," files for ","."],"values":[3,"Ada"]}

The tag receives one more literal segment than interpolated values. record stores both arrays rather than flattening the number into text. A later logger could attach searchable fields or choose how to render the message. Even a template without interpolation calls the tag, with one literal segment and no values.

The return value here is an object; a tag does not have to return a string. JavaScript supplies a frozen literal-strings array for the template site, so the example copies it before storing it as ordinary application data. The interpolated values are still ordinary references and are not deep-cloned. See the tagged template reference.

2. Encode individual URL path segments

Save this as encode-template-path.mjs:

import assert from 'node:assert/strict';

function path(strings, ...segments) {
  let result = strings[0];
  for (let index = 0; index < segments.length; index += 1) {
    if (typeof segments[index] !== 'string') {
      throw new TypeError('path segments must be strings');
    }
    result += encodeURIComponent(segments[index]) + strings[index + 1];
  }
  return result;
}
const result = path`/docs/${'JavaScript basics'}/${'chapter/one'}`;
assert.equal(result, '/docs/JavaScript%20basics/chapter%2Fone');
assert.equal(path`/docs/${'a?draft=true'}`, '/docs/a%3Fdraft%3Dtrue');
assert.equal(path`/docs/${''}`, '/docs/');
assert.throws(() => path`/docs/${null}`, TypeError);
console.log(result);
console.log(path`/docs/${'a?draft=true'}`);

Run it:

node encode-template-path.mjs

Output:

/docs/JavaScript%20basics/chapter%2Fone
/docs/a%3Fdraft%3Dtrue

The trusted literal text supplies separators. Each interpolation is one path segment, so spaces, embedded slashes, and question marks are encoded rather than treated as separators or a query. Encoding the whole result would also encode the literal slashes, which is not the intended operation. See encodeURIComponent in ECMAScript.

This tag has a deliberately narrow contract. It does not validate destination hosts, prevent traversal through . or .., or guarantee how a server will decode encoded slashes. Do not use it for authorization or safe filesystem paths. URL encoding is also not HTML escaping or SQL parameter binding; those contexts require their own APIs. A tag name alone provides no safety guarantee.

3. Build a raw regular-expression pattern

Save this as preserve-raw-pattern.mjs:

import assert from 'node:assert/strict';

function digitCode(length) {
  if (!Number.isInteger(length) || length < 1 || length > 6) {
    throw new RangeError('length must be an integer from 1 to 6');
  }
  const pattern = String.raw`^\d{${length}}$`;
  return new RegExp(pattern);
}
const code = digitCode(3);
assert.equal(code.source, '^\\d{3}$');
assert.equal(code.test('123'), true);
assert.equal(code.test('12'), false);
assert.equal(code.test('12a'), false);
assert.equal(code.test('x123'), false);
assert.equal(code.test('123\n'), false);
assert.throws(() => digitCode('3'), RangeError);
assert.throws(() => digitCode(0), RangeError);
console.log(`Pattern: ${code.source}`);
console.log(`123 matches: ${code.test('123')}`);
console.log(`12a matches: ${code.test('12a')}`);

Run it:

node preserve-raw-pattern.mjs

Output:

Pattern: ^\d{3}$
123 matches: true
12a matches: false

String.raw is a built-in tag that keeps the literal segment’s backslash, so \d reaches the RegExp constructor. Interpolation still happens: the validated number becomes the repetition count. The assertion uses an ordinary quoted string, where writing the same backslash requires \\.

Raw text does not mean escaped input. Interpolating arbitrary user text into a regular expression could change its meaning or create an expensive pattern. This helper accepts only a small integer range. For arbitrary literal search text, use a suitable escaping API rather than assuming String.raw sanitizes it. The String.raw specification defines how raw literal segments and substitutions are joined.

Clean up

In the same terminal, remove only the named files, return to the original directory, and remove the now-empty scratch directory:

rm -- capture-structured-log.mjs encode-template-path.mjs preserve-raw-pattern.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on