When you call a JavaScript tagged template such as tag`a${x}b`, what arguments does the tag function actually receive, and what is the relationship between them?
answer
- Static text and values arrive separately
- First argument is an array, not a string
- Count the gaps, not the placeholders
- strings.length is values.length plus one
- raw holds the uninterpreted escapes
basics
~20 sThe tag function receives the static text chunks as its first argument — an array with a raw property holding the unescaped text — followed by one argument per interpolated value. The chunks array always has exactly one more element than the number of values.
solid answer
~50 sWriting `` tag`a${x}b` `` calls `tag(['a','b'], x)`. The first argument is the *template object*: an array of the literal text chunks between the placeholders, carrying an extra `raw` property whose entries hold the same chunks with backslash escapes left uninterpreted. The remaining arguments are the evaluated placeholder values, usually collected as `...values`. The invariant to state out loud is `strings.length === values.length + 1`, which holds because the text is split at each placeholder; a template that starts or ends with a placeholder simply gets an empty string at that edge, so `` tag`${x}` `` sees `['','']`. The values are evaluated eagerly, left to right, before the tag runs. Critically, the tag may return **anything** — a string, an object, a DOM-free data structure — because the tagged form is a function call, not a string literal.
code
javascript · 13 linesfunction inspect(strings, ...values) {
console.log('cooked:', strings);
console.log('raw :', strings.raw);
console.log('values:', values);
console.log('invariant:', strings.length === values.length + 1);
return strings.reduce(
(out, chunk, i) => out + chunk + (i < values.length ? String(values[i]) : ''),
''
);
}
const who = 'Ada';
console.log(inspect`Hi\t${who}, ${1 + 1} items`);go deeper
Recall that putting a function name directly before a backtick string calls that function, and that it gets the literal text separately from the interpolated values.
Be able to write the identity tag on a whiteboard and state strings.length === values.length + 1, explaining the empty edge strings and what strings.raw holds.
Show why the separation matters in production: the tag is the only place that can distinguish author-written text from runtime data, which is what makes escaping and parameterisation trustworthy.
Weigh whether a tagged DSL is worth it at all — the readability gain against debuggability, tooling and type support, and the cost of a house tag everyone must learn.
## The shape of the call Putting a function reference directly before a template literal turns the literal into a **function call**: ```js function tag(strings, ...values) { return { strings, raw: strings.raw, values }; } const who = 'Ada', n = 2; tag`Hi ${who}, you have ${n} items.`; // strings -> ['Hi ', ', you have ', ' items.'] // values -> ['Ada', 2] ``` The literal is cut at every placeholder. The pieces of *static* text become the first argument; the *evaluated* placeholder expressions become the rest. That separation is the whole point: the tag can tell, with certainty, which characters came from the source file and which came from data. ## The length invariant `strings.length === values.length + 1`, always. With k placeholders there are k+1 gaps of static text, some possibly empty: ```js tag`${a}`; // strings: ['', ''], values: [a] tag`x${a}`; // strings: ['x', ''], values: [a] tag`${a}${b}`; // strings: ['', '', ''], values: [a, b] tag`plain`; // strings: ['plain'], values: [] ``` Writing a tag as a loop over `values` and reaching for `strings[i]` and `strings[i + 1]` therefore never runs off the end; the idiomatic body walks `strings` and appends `values[i]` while `i < values.length`. ## The raw property The first argument is a genuine array (`Array.isArray(strings)` is `true`) with one extra own property, `raw`, itself an array of the same length. `strings[i]` holds the **cooked** text — backslash escapes already interpreted — while `strings.raw[i]` holds the characters as written: ```js function show(s) { return [s[0], s.raw[0]]; } show`a\nb`; // ['a\nb' (3 chars, real newline), 'a\\nb' (4 chars)] ``` A tag that wants to see exactly what the author typed — a path, a regular-expression source, a DSL fragment — reads `raw`. A tag that wants normal string semantics reads the array itself. Since ES2018 ("template literal revision"), an invalid escape sequence inside a *tagged* template no longer makes the whole thing a syntax error: the corresponding cooked entry is `undefined` while `raw` still carries the text. That is what lets tags host foreign syntaxes where `\u` or `\x` mean something else. In an untagged template, an invalid escape is still a `SyntaxError`. ## Eager evaluation, arbitrary return The placeholder expressions are evaluated **before** the tag is entered, left to right — a tag cannot skip, defer, or re-order them, and a side effect inside a placeholder happens whether or not the tag uses that value. What the tag *can* do is decide what each value contributes to the result, or not use it at all. The return value is unconstrained. Most tags return a string, but returning a structured object is a legitimate and common design: a query tag can return `{ text: 'select * from t where id = $1', values: [id] }`, keeping data out of the text entirely; a markup tag can return a description of the fragment rather than a string. Because it is a plain function call, a tag can also be a method (`sql.raw`...), be curried, or close over configuration. ## Writing one The minimal identity tag reconstructs what a plain template would have produced: ```js function identity(strings, ...values) { return strings.reduce( (out, chunk, i) => out + chunk + (i < values.length ? String(values[i]) : ''), '' ); } ``` From that skeleton, every useful tag is a change to how `values[i]` is turned into text: escape it, quote it, register it as a bound parameter, format it by locale, or replace it with a placeholder token. ## Details worth knowing - The template object is **frozen**, along with its `raw` array; a tag cannot mutate the chunks it was handed. - The same call site hands the tag the same array object each time it executes, which is what makes per-site caching possible. - Any callable works as a tag, including an arrow function and an object method, and the tag expression may itself be a member expression (`obj.tag\`...\``), in which case `this` is `obj`. - Tags compose only manually: there is no built-in way to feed one tag's output into another as a template. Being able to sketch the identity tag and recite the length invariant is the substance of this question; the rest follows from it.
- Why does the strings array always have one more element than the values array?Because the literal is split *at* each placeholder: k placeholders create k+1 stretches of static text. When the template starts or ends with a placeholder, the stretch at that edge is the empty string rather than being omitted, so `` tag`${x}` `` receives `['','']`. Keeping the empty entries preserves the invariant and lets a tag loop without bounds checks.
- Does a tag function have to return a string?No — a tagged template is an ordinary function call, so the return value is whatever the tag returns. Returning a structured object is a real pattern: a query tag can return the static text with numbered placeholders plus a separate values array, so the data never enters the text. Returning objects, numbers, or nothing at all is all legal.
- Can a tag function avoid evaluating a placeholder it does not need?No. All placeholder expressions are evaluated left to right before the tag is called, so their side effects have already happened. A tag can ignore a value's *contribution* to the result, but it cannot make the expression lazy — wrap it in a function and interpolate the function if you need deferral.
saying these in an interview costs you the question
- Says the tag receives the whole template as one string
- Thinks strings and values arrays have equal length
- Believes placeholders are evaluated lazily inside the tag
- Confuses strings.raw with the interpolated values
- Assumes a tag must return a string