Template Literals in TypeScript 📚✍️
Matt Lewandowski
Posted on August 6, 2024
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
- Introduction to Template Literals
- Basic Syntax and Usage
- Multiline Strings
- Expression Interpolation
- Tagged Templates
- Real-world Use Cases
- Template Literals vs. Traditional String Concatenation
- Advanced Techniques
- Best Practices and Potential Pitfalls
- 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:
- Cumbersome string concatenation
- Lack of native multiline string support
- 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!
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: $
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);
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');
})();
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.
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.
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.
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}`;
Let's break down the components:
Tag Function: This is a regular function, but when used as a tag, it's called without parentheses.
-
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.
- The first parameter (
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 ]
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>
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:
Custom String Interpolation: You can define how values are interpolated into the string, allowing for complex transformations.
DSL Creation: Tagged templates can be used to create domain-specific languages (DSLs) within JavaScript/TypeScript.
Security: They can be used to sanitize input, preventing injection attacks in contexts like SQL queries or HTML generation.
Localization: Tagged templates can facilitate internationalization by processing strings through translation functions.
Syntax Highlighting: In certain environments, tagged templates can enable syntax highlighting for embedded languages.
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));
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;
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.
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>
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.
Template literals offer several advantages:
- Readability: Template literals are often easier to read, especially for complex strings.
- Less error-prone: No need to worry about missing spaces or closing quotes.
- Multiline support: No need for '\n' or concatenation for multiline strings.
- Expression interpolation: Can include complex expressions directly in the string.
However, traditional concatenation might be preferred in some cases:
- When working with older codebases or environments that don't support ES6.
- For very simple string manipulations where template literals might be overkill.
- 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
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);
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
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' });
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)
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)
Posted on August 6, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.