TypeScript Template Strings: Interpolation, Tags and Types

How template strings work in TypeScript: placeholders and expressions, multi-line text, tagged templates and String.raw, and template literal types that check string patterns at compile time.

angular-typescript

A template string, also called a template literal, is a string written between backticks (`) instead of quotes. Inside it, a placeholder of the form ${expression} inserts the value of any expression into the text, and line breaks are kept as typed. Building a string this way is called string interpolation, and it replaces long chains of + concatenation. TypeScript supports the JavaScript feature unchanged and adds template literal types, which use the same backtick syntax to build string types such as “size-small” | “size-large”.

We use template strings whenever we build text from values, such as a log line with the user’s name and the order total, or a small HTML fragment for a product list.

The following example shows placeholders, expressions, line breaks, the built-in String.raw tag and a template literal type, with the result of each line as a comment. The snippets run with TypeScript 7.0.2 on Node.js 22.

const name = "Lokesh";
const age = 37;

// 1. Placeholders
const intro = `My name is ${name} and I am ${age}`;   // intro = "My name is Lokesh and I am 37"

// 2. Any expression
const next = `Next year: ${age + 1}`;         // next = "Next year: 38"
const group = `Group: ${age >= 18 ? "adult" : "minor"}`;   // group = "Group: adult"

// 3. Multi-line string
const lines = `apple
banana`;                                      // lines = "apple\nbanana"

// 4. Built-in tag: backslashes stay as typed
const path = String.raw`C:\new\folder`;       // path = "C:\new\folder"

// 5. Template literal type
type Size = "small" | "large";
type SizeClass = `size-${Size}`;              // "size-small" | "size-large"
const css: SizeClass = "size-small";          // "size-medium" is a compile error

Notice that String.raw keeps the backslashes in path as typed, whereas a plain template string turns \n into a line break.

After the syntax and its escape rules, we look at expressions in placeholders and at how values turn into text. The later sections cover multi-line strings, tagged templates, and template literal types with the errors the compiler reports for them.

1. Template String Syntax

A template string starts and ends with a backtick, the key left of 1 on a US keyboard. Each ${…} placeholder is evaluated when the line runs, and its result is converted to a string and inserted at that position. The result is an ordinary string, and TypeScript infers the type string for it, so we can pass it anywhere a string is expected. For example, an order service that writes “Order 1001 shipped to Lokesh” to the log builds that line with one template string instead of four + operators.

const name = "Lokesh";
const age = 37;

// 1. Concatenation
const s1 = "My name is " + name + " and I am " + age;

// 2. Template string
const s2 = `My name is ${name} and I am ${age}`;
// s1 and s2 = "My name is Lokesh and I am 37"

// 3. Backticks and placeholders as plain text
const quote = `Use \`backticks\` here`;       // quote = "Use `backticks` here"
const literal = `Price: \${10}`;              // literal = "Price: ${10}"

Both lines build the same string, but the template string keeps the spaces next to the text they belong to, which removes the most common concatenation bug, namely a missing space before or after a variable. To write a backtick inside a template string, we escape it with a backslash, and to write the characters ${ as text, we escape the dollar sign. Quotes need no escaping, so template strings are also convenient for text that contains both ‘ and “.

2. Expressions in Placeholders

A placeholder accepts any JavaScript expression, such as a calculation, a method call, a ternary condition or another template string. Statements such as if or for are not allowed, because a statement does not produce a value. For anything longer than one short expression, we compute the value in a variable before the template, which keeps the line readable.

const fruits = ["apple", "banana"];
const lokesh = { name: "Lokesh", age: 37 };
const nicknames = new Map<string, string>();

// 1. Calculations, calls and conditions
const total = `Total: ${(0.1 + 0.2).toFixed(2)}`;    // total = "Total: 0.30"
const upper = `Name: ${lokesh.name.toUpperCase()}`;  // upper = "Name: LOKESH"
const count = `${fruits.length} ${fruits.length === 1 ? "fruit" : "fruits"}`;   // count = "2 fruits"

// 2. Nested templates
const html = `<ul>${fruits.map((f) => `<li>${f}</li>`).join("")}</ul>`;
// html = "<ul><li>apple</li><li>banana</li></ul>"

// 3. How values become text
const list = `${fruits}`;                     // list = "apple,banana"
const obj = `${lokesh}`;                      // obj = "[object Object]"
const json = `${JSON.stringify(lokesh)}`;     // json = "{"name":"Lokesh","age":37}"
const nickname = nicknames.get("Lokesh");
const empty = `Hi ${nickname}`;               // empty = "Hi undefined"
const safe = `Hi ${nickname ?? "friend"}`;    // safe = "Hi friend"

Step 3 shows how JavaScript converts each kind of value to text.

  • An array is joined with commas.
  • A plain object becomes “[object Object]”.
  • The values null and undefined appear as the words “null” and “undefined”.

The compiler accepts all of these, because any value can be converted to a string. For objects we call JSON.stringify() or pick the fields we need, and for values that can be missing we add a default with the ?? operator, one of the TypeScript logical operators. For example, a welcome banner that reads the nickname of a new user from a map shows “Hi undefined” until we add the default.

The compiler rejects one type, which is symbol. Converting a symbol to a string implicitly throws a TypeError at runtime, so `Key: ${id}` with const id = Symbol(“id”) fails with error TS2731, “Implicit conversion of a ‘symbol’ to a ‘string’ will fail at runtime. Consider wrapping this expression in ‘String(…)’.” To catch “[object Object]” output as well, projects use the typescript-eslint rule restrict-template-expressions, which reports objects and arrays in placeholders.

3. Multi-Line Strings

A template string can span several lines. Each line break in the source becomes a newline character in the value, so we don’t need \n or joining lines with +. The problem is that the indentation of each line becomes part of the string too.

// 1. Line breaks and indentation are part of the string
const list = `Fruits:
  - apple
  - banana`;
// list = "Fruits:\n  - apple\n  - banana"

// 2. Build lines without extra spaces
const rows = ["Fruits:", "- apple", "- banana"].join("\n");
// rows = "Fruits:\n- apple\n- banana"

When the template sits inside an indented function, every line after the first starts with that function’s indentation. For output where spaces matter, such as a CSV file or a fixed-format message, we either write the continuation lines at column 0 as in the intro example, or build the lines in an array and join() them.

4. Tagged Templates

A tagged template is a template string with a function name written right before it, without parentheses. JavaScript calls that function, the tag, with two kinds of arguments, first an array of the text parts and after it the placeholder values. The tag returns the final result, which can be a string or any other value. TypeScript provides the type TemplateStringsArray for the first parameter.

function upper(strings: TemplateStringsArray, ...values: unknown[]): string {
  let result = strings[0];
  values.forEach((value, i) => {
    result += String(value).toUpperCase() + strings[i + 1];
  });
  return result;
}

const name = "Lokesh";
const city = "Delhi";

// strings = ["", " lives in ", "."], values = ["Lokesh", "Delhi"]
const message = upper`${name} lives in ${city}.`;   // message = "LOKESH lives in DELHI."

// String.raw keeps backslashes
const path = String.raw`C:\new\folder`;       // path = "C:\new\folder"
const normal = `C:\new\folder`;               // \n becomes a line break

The text array always has one more element than the values array, because the text before the first placeholder and after the last one are included even when empty. For that reason, the loop in upper() starts with strings[0] and adds strings[i + 1] after each value. The …values parameter is a rest parameter that collects all placeholder values.

Libraries use tags to escape HTML or to build SQL queries with bound parameters, and CSS-in-JS libraries style components with them. For example, a sql tag in a reporting app turns each placeholder into a bound parameter, so a name typed into a search box never becomes part of the SQL text. The built-in tag String.raw returns the text with escape sequences left as typed, which is useful for Windows paths and regular expressions. In normal, the \n and \f in the path become a newline and a form feed.

The type of the rest parameter controls what the placeholders may contain. A tag declared with …values: number[] rejects a string placeholder at compile time with error TS2345, “Argument of type ‘string’ is not assignable to parameter of type ‘number’.”

5. Template Literal Types

Template literal types, added in TypeScript 4.1, use the backtick syntax at the type level. A placeholder holds a type instead of a value, and the result is a new string literal type. When a placeholder holds a union type, TypeScript produces every combination of the literal types in it. For example, a design system that allows only the CSS classes size-small and size-large can reject size-medium at compile time.

type Size = "small" | "large";
type Color = "red" | "blue";

// 1. Build string types from other types
type SizeClass = `size-${Size}`;              // "size-small" | "size-large"
type Combo = `${Size}-${Color}`;              // "small-red" | "small-blue" | "large-red" | "large-blue"

// 2. Patterns with number and string
type Px = `${number}px`;
const width: Px = "10px";                     // "wide" is a compile error

// 3. Change case with intrinsic types
type Loud = Uppercase<"hello">;               // "HELLO"
type Getter = `get${Capitalize<"name">}`;     // "getName"

// 4. Generate method names from keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type PersonGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }

// 5. Read parts of a string type with infer
type Split<S> = S extends `${infer K}:${infer V}` ? [K, V] : never;
type Pair = Split<"Lokesh:37">;               // ["Lokesh", "37"]

Uppercase, Lowercase, Capitalize and Uncapitalize are intrinsic types, which are built into the compiler and change the case of a string literal type. Step 4 combines them with a mapped type that renames keys, so the getter type changes whenever the object type changes. Step 5 uses a conditional type with infer to read the parts before and after the colon.

These types exist only at compile time; at runtime the values are plain strings. The compiler rejects every string that does not match the pattern, and for a union it suggests the closest member when one exists.

type Size = "small" | "large";
type Color = "red" | "blue";
type SizeClass = `size-${Size}`;
type Combo = `${Size}-${Color}`;
type Px = `${number}px`;

const c1: SizeClass = "size-medium";
const c2: Combo = "small-green";
const w: Px = "wide";
src/err.ts(7,7): error TS2322: Type '"size-medium"' is not assignable to type '"size-large" | "size-small"'.
src/err.ts(8,7): error TS2820: Type '"small-green"' is not assignable to type '"large-blue" | "large-red" | "small-blue" | "small-red"'. Did you mean '"small-red"'?
src/err.ts(9,7): error TS2322: Type '"wide"' is not assignable to type '`${number}px`'.

Combining unions multiplies their sizes, so two unions of 10 members each produce 100 string types. Very large combinations slow down the compiler, so we keep the input unions small.

6. Download and Run the Code

The template-strings directory in the GitHub repository contains every snippet that compiles and writes each commented value to the console, including what a tag function receives. Its package.json pins TypeScript 7.0.2, and the start script needs Node.js 22 or later.

npm install
npm start

7. Conclusion

Template strings make string building shorter and clearer. Placeholders take any expression and line breaks are kept as typed. To write a backtick or a placeholder as text, we escape it with a backslash.

Values are converted with their default string form, so objects and missing values need JSON.stringify() or a ?? default. Tagged templates pass the text parts and values to a function such as String.raw. At the type level, template literal types build and check string patterns, which lets the compiler catch a wrong CSS class or event name before the code runs.

8. References

MDN covers the runtime syntax and tagged templates; the TypeScript Handbook covers template literal types and the related mapped and conditional types.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.