Template Literals in TypeScript πŸ“šβœοΈ

Matt Lewandowski - Aug 6 - - Dev Community

Template literals have changed string manipulation in JavaScript and TypeScript, offering a more expressive and powerful way to work with text. This article will dive into template literals, exploring their features, use cases, and some advanced techniques. Whether you want to enhance your string handling skills or better understand the libraries you use daily, this guide has something for you.

Table of Contents

  1. Introduction to Template Literals
  2. Basic Syntax and Usage
  3. Multiline Strings
  4. Expression Interpolation
  5. Tagged Templates
  6. Real-world Use Cases
  7. Template Literals vs. Traditional String Concatenation
  8. Advanced Techniques
  9. Best Practices and Potential Pitfalls
  10. The End

Introduction to Template Literals

Template literals, introduced in ECMAScript 6 (ES6) and fully supported in TypeScript, provide a more flexible and readable way to create strings. They allow for easy string interpolation, multiline strings, and even function-based string manipulation through tagged templates.

The introduction of template literals addressed several long-standing issues with string manipulation in JavaScript:

  1. Cumbersome string concatenation
  2. Lack of native multiline string support
  3. Limited string interpolation capabilities

Let's explore how template literals solve these problems and introduce new possibilities for string handling in TypeScript.

Basic Syntax and Usage

Template literals are enclosed by backticks (`) instead of single or double quotes. Here's a simple example:

const name: string = "Alice";
const greeting: string = `Hello, ${name}!`;
console.log(greeting); // Output: Hello, Alice!
Enter fullscreen mode Exit fullscreen mode

This syntax allows for seamless embedding of expressions within strings, making your code more readable and maintainable. The ${} syntax is used for expression interpolation, which we'll explore in more depth later.

Escaping in Template Literals

To include a backtick in a template literal, you can use the backslash (\) as an escape character:

const message: string = `This is a backtick: \` and this is a dollar sign: \$`;
console.log(message); // Output: This is a backtick: ` and this is a dollar sign: $
Enter fullscreen mode Exit fullscreen mode

Multiline Strings

One of the most appreciated features of template literals is the ability to create multiline strings without concatenation or escape characters:

const multilineString: string = `
  This is a multiline string.
  It can span multiple lines
  without any special characters.
`;
console.log(multilineString);
Enter fullscreen mode Exit fullscreen mode

This feature is particularly useful when working with HTML templates, SQL queries, or any text that naturally spans multiple lines. It significantly improves code readability and reduces the likelihood of errors that can occur when manually concatenating multiline strings.

Controlling Whitespace

While multiline strings in template literals preserve line breaks, they also preserve leading whitespace. This can sometimes lead to unintended formatting. You can use various techniques to control whitespace:

// Using string methods
const trimmedString: string = `
  This string's leading and trailing whitespace
  will be removed.
`.trim();

// Using an immediately-invoked function expression (IIFE)
const formattedString: string = (() => {
  const lines = `
    This string
    will have consistent
    indentation
  `;
  return lines.split('\n').map(line => line.trim()).join('\n');
})();
Enter fullscreen mode Exit fullscreen mode

Expression Interpolation

Template literals shine when it comes to embedding expressions. You can include any valid JavaScript expression within ${}:

const a: number = 5;
const b: number = 10;
console.log(`The sum of ${a} and ${b} is ${a + b}.`);
// Output: The sum of 5 and 10 is 15.

const user = { name: "Bob", age: 30 };
console.log(`${user.name} is ${user.age} years old.`);
// Output: Bob is 30 years old.
Enter fullscreen mode Exit fullscreen mode

This feature allows for more complex logic within your strings:

const isAdult = (age: number): boolean => age >= 18;
const userAge: number = 20;
console.log(`User is ${isAdult(userAge) ? "an adult" : "a minor"}.`);
// Output: User is an adult.
Enter fullscreen mode Exit fullscreen mode

Nested Templates

You can even nest template literals within each other:

const nestedTemplate = (x: number, y: number): string => `The result is ${`${x + y}`}.`;
console.log(nestedTemplate(5, 10)); // Output: The result is 15.
Enter fullscreen mode Exit fullscreen mode

While powerful, be cautious with nesting to maintain code readability.

Tagged Templates

Tagged templates are one of the most powerful yet underutilized features of template literals. They allow you to define a function to process the template literal, giving you access to both the string parts and the interpolated expressions.

How Tagged Templates Work

A tagged template is essentially a function call, but with a special syntax. The function (often called a "tag function") is called with the template literal as its arguments. Here's the general structure:

function tagFunction(strings: string[], ...values: any[]): any {
  // Process the strings and values
  // Return the result
}

// Usage
const result = tagFunction`Some ${value} and ${anotherValue}`;
Enter fullscreen mode Exit fullscreen mode

Let's break down the components:

  1. Tag Function: This is a regular function, but when used as a tag, it's called without parentheses.

  2. Parameters:

    • The first parameter (strings) is an array of string literals from the template.
    • The rest parameter (...values) is an array of the interpolated values.
  3. Return Value: The tag function can return any type, not just a string.

Here's a simple example to illustrate how the parameters are populated:

function logParts(strings: string[], ...values: any[]): void {
  console.log("Strings:", strings);
  console.log("Values:", values);
}

const x = 10;
const y = 20;
logParts`The sum of ${x} and ${y} is ${x + y}.`;

// Output:
// Strings: [ 'The sum of ', ' and ', ' is ', '.' ]
// Values: [ 10, 20, 30 ]
Enter fullscreen mode Exit fullscreen mode

Notice how the string is split around the interpolated values, and the values are passed as separate arguments.

Creating Reusable Tagged Template Functions

You can create functions that return tag functions, allowing for more flexible and reusable tagged templates:

function createHighlighter(highlightColor: string) {
  return function(strings: TemplateStringsArray, ...values: any[]): string {
    return strings.reduce((result, str, i) => {
      const value = values[i] || '';
      const highlighted = `<span style="background-color: ${highlightColor}">${value}</span>`;
      return `${result}${str}${highlighted}`;
    }, '');
  }
}

const highlightRed = createHighlighter('red');
const highlightYellow = createHighlighter('yellow');

console.log(highlightRed`Important: ${100}`);
console.log(highlightYellow`Warning: ${'Caution'}`);

// Output:
// Important: <span style="background-color: red">100</span>
// Warning: <span style="background-color: yellow">Caution</span>
Enter fullscreen mode Exit fullscreen mode

This approach allows you to create customized tag functions on the fly.

Why Tagged Templates Exist

Tagged templates were introduced to provide a powerful way to process template literals. They offer several advantages:

  1. Custom String Interpolation: You can define how values are interpolated into the string, allowing for complex transformations.

  2. DSL Creation: Tagged templates can be used to create domain-specific languages (DSLs) within JavaScript/TypeScript.

  3. Security: They can be used to sanitize input, preventing injection attacks in contexts like SQL queries or HTML generation.

  4. Localization: Tagged templates can facilitate internationalization by processing strings through translation functions.

  5. Syntax Highlighting: In certain environments, tagged templates can enable syntax highlighting for embedded languages.

  6. Compile-time Checks: With TypeScript, you can perform compile-time checks on the interpolated values.

Real-world Use Cases

Let's explore some practical applications of template literals and tagged templates:

1. HTML Generation

Template literals are excellent for generating HTML:

function createUserCard(user: { name: string; email: string; role: string }): string {
  return `
    <div class="user-card">
      <h2>${user.name}</h2>
      <p>Email: ${user.email}</p>
      <p>Role: ${user.role}</p>
    </div>
  `;
}

const user = { name: "Alice", email: "alice@example.com", role: "Developer" };
console.log(createUserCard(user));
Enter fullscreen mode Exit fullscreen mode

2. SQL Query Building

Tagged templates can be used to safely construct SQL queries:

function sql(strings: TemplateStringsArray, ...values: any[]): string {
  return strings.reduce((query, str, i) => {
    const value = values[i] || '';
    const escapedValue = typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value;
    return `${query}${str}${escapedValue}`;
  }, '');
}

const table: string = "users";
const name: string = "O'Brien";
const age: number = 30;

const query: string = sql`SELECT * FROM ${table} WHERE name = ${name} AND age > ${age};`;
console.log(query);
// Output: SELECT * FROM users WHERE name = 'O''Brien' AND age > 30;
Enter fullscreen mode Exit fullscreen mode

3. Internationalization (i18n)

Tagged templates can facilitate internationalization:

function i18n(strings: TemplateStringsArray, ...values: any[]): string {
  // This is a simplified example. In a real-world scenario,
  // you would use a proper i18n library or service.
  const translations: { [key: string]: string } = {
    "Hello": "Bonjour",
    "world": "monde",
    "Welcome to": "Bienvenue Γ "
  };

  return strings.reduce((result, str, i) => {
    const value = values[i] || '';
    const translated = translations[String(value)] || value;
    return `${result}${translations[str.trim()] || str}${translated}`;
  }, '');
}

const name: string = "Alice";
const place: string = "Paris";
console.log(i18n`Hello, ${name}! Welcome to ${place}.`);
// Output: Bonjour, Alice! Bienvenue Γ  Paris.
Enter fullscreen mode Exit fullscreen mode

4. Styled Components (simplified version)

Here's a basic implementation of how libraries like styled-components use tagged templates:

function css(strings: TemplateStringsArray, ...values: any[]): string {
  return strings.reduce((result, str, i) => {
    const value = values[i] || '';
    return `${result}${str}${value}`;
  }, '');
}

function styled(tagName: string) {
  return (cssTemplate: TemplateStringsArray, ...cssValues: any[]) => {
    const cssString = css(cssTemplate, ...cssValues);
    return (strings: TemplateStringsArray, ...values: any[]): string => {
      const content = strings.reduce((result, str, i) => {
        const value = values[i] || '';
        return `${result}${str}${value}`;
      }, '');
      return `<${tagName} style="${cssString}">${content}</${tagName}>`;
    };
  };
}

const Button = styled('button')`
  background-color: blue;
  color: white;
  padding: 10px 20px;
`;

console.log(Button`Click me!`);
// Output: <button style="background-color: blue;color: white;padding: 10px 20px;">Click me!</button>
Enter fullscreen mode Exit fullscreen mode

Template Literals vs. Traditional String Concatenation

Let's compare template literals with traditional string concatenation:

const name: string = "Alice";
const age: number = 30;

// Traditional concatenation
const traditionalString: string = "My name is " + name + " and I am " + age + " years old.";

// Template literal
const templateString: string = `My name is ${name} and I am ${age} years old.`;

console.log(traditionalString);
console.log(templateString);
// Both output: My name is Alice and I am 30 years old.
Enter fullscreen mode Exit fullscreen mode

Template literals offer several advantages:

  1. Readability: Template literals are often easier to read, especially for complex strings.
  2. Less error-prone: No need to worry about missing spaces or closing quotes.
  3. Multiline support: No need for '\n' or concatenation for multiline strings.
  4. Expression interpolation: Can include complex expressions directly in the string.

However, traditional concatenation might be preferred in some cases:

  1. When working with older codebases or environments that don't support ES6.
  2. For very simple string manipulations where template literals might be overkill.
  3. In performance-critical sections where string concatenation might be marginally faster (though the difference is usually negligible).

Advanced Techniques

1. Template Literal Types in TypeScript

TypeScript 4.1 introduced template literal types, allowing you to use template literal syntax in type definitions:

type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';

type ColorSize = `${Color}-${Size}`;

// ColorSize is now equivalent to:
// 'red-small' | 'red-medium' | 'red-large' | 'blue-small' | 'blue-medium' | 'blue-large' | 'green-small' | 'green-medium' | 'green-large'

const myColor: ColorSize = 'blue-medium'; // Valid
// const invalidColor: ColorSize = 'yellow-extra-large'; // Error
Enter fullscreen mode Exit fullscreen mode

2. Using Template Literals with Map

Template literals can be powerful when used with array methods:

const fruits: string[] = ["apple", "banana", "cherry"];
const fruitList: string = `
  <ul>
    ${fruits.map(fruit => `<li>${fruit}</li>`).join('')}
  </ul>
`;
console.log(fruitList);
Enter fullscreen mode Exit fullscreen mode

3. Dynamic Property Access

Template literals can be used for dynamic property access:

const user = { name: "Alice", age: 30, role: "Developer" };
const getProperty = (obj: any, prop: string): string => `${obj[prop]}`;

console.log(getProperty(user, "name")); // Output: Alice
console.log(getProperty(user, "age"));  // Output: 30
Enter fullscreen mode Exit fullscreen mode

4. Template Compilation

Tagged templates can be used to create reusable template functions:

function compile<T extends Record<string, any>>(
  strings: TemplateStringsArray,
  ...keys: (keyof T)[]
): (data: T) => string {
  return (data: T): string => {
    return strings.reduce((result, str, i) => {
      const key = keys[i];
      const value = key ? data[key] : '';
      return `${result}${str}${value}`;
    }, '');
  };
}

const greet = compile<{ name: string; age: number }>`Hello, ${'name'}! You are ${'age'} years old.`;

console.log(greet({ name: 'Alice', age: 30 }));
// Output: Hello, Alice! You are 30 years old.

// This would cause a compile-time error:
// greet({ name: 'Bob', age: '30' });
Enter fullscreen mode Exit fullscreen mode

This technique allows you to create type-safe, reusable template functions.

5. Raw Strings

Template literals provide access to the raw strings through the raw property of the TemplateStringsArray:

function rawTag(strings: TemplateStringsArray, ...values: any[]): string {
  return strings.raw.reduce((acc, str, i) => acc + str + (values[i] || ''), '');
}

console.log(rawTag`Hello\nWorld`);
// Output: Hello\nWorld (not Hello[newline]World)
Enter fullscreen mode Exit fullscreen mode

This can be useful when you need to preserve backslashes, such as when working with regular expressions or file paths.

The End

Template literals have significantly improved string handling in TypeScript and JavaScript. They offer a more intuitive and powerful way to work with strings, from simple interpolation to complex string processing with tagged templates. By leveraging template literals, you can write cleaner, more expressive code that's easier to maintain and understand.

As you continue to work with template literals, you'll likely discover even more creative ways to use them in your projects. Remember to balance their power with readability and maintainability, and you'll find them to be an invaluable tool in your TypeScript toolkit.

Oh and one last shameless plug 😁
If you work in an agile dev team, check out my [free planning poker and retrospective app called Kollabe].(https://kollabe.com/retrospectives)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .