Skip to main content

TypeScript Multiline Strings

You're building an error message that needs to include a user's name, the specific action that failed, and a timestamp. Using string concatenation with + and \n quickly becomes a mess of quotes and escape characters. Or you're crafting an email template where readability matters, but your code looks like tangled spaghetti.

Template literals solve this. They let you write strings across multiple lines exactly as they'll appear, embed variables directly into the text, and keep your code readable. If you've been fighting with string concatenation or wondering why your multiline strings look terrible, this guide will show you how template literals make working with complex text straightforward.

Creating Multiline Strings with Template Literals

To make a multiline string in TypeScript, you use template literals, which are created by wrapping your string in backticks (`) instead of quotes. This lets you write your string over multiple lines without needing to join them or use special newline characters.

const multilineString = `
This is a multiline string.
It can span multiple lines.
`;

console.log(multilineString);

This will output the string exactly as written, preserving the line breaks. Unlike older JavaScript approaches that required concatenation with \n characters, template literals make multiline text straightforward and readable.

Template Literals vs. String Concatenation

When should you use template literals instead of the traditional + operator? Here's a practical comparison.

The Old Way: String Concatenation

const username = "Sarah";
const action = "file upload";
const timestamp = new Date().toISOString();

const errorMessage = "Error: " + action + " failed for user " + username +
" at " + timestamp + ".\n" +
"Please check your connection and try again.\n" +
"If the problem persists, contact support.";

This works, but it's hard to read and easy to mess up. You have to track quotes, plus signs, and explicit \n characters. One missing space or quote breaks everything.

The Modern Way: Template Literals

const username = "Sarah";
const action = "file upload";
const timestamp = new Date().toISOString();

const errorMessage = `Error: ${action} failed for user ${username} at ${timestamp}.
Please check your connection and try again.
If the problem persists, contact support.`;

The structure is clearer. Variables slot in naturally with ${}, and line breaks are automatic. You can see how the final message will look just by reading the code.

When to Use Each Approach

Use template literals when:

  • Working with multiline strings
  • Embedding multiple variables or expressions
  • Building HTML, SQL queries, or formatted text
  • Readability matters (which is almost always)

Use concatenation when:

  • You need compatibility with very old JavaScript environments (pre-ES6)
  • Joining a simple string with one variable where template literal syntax feels excessive
  • Working with a codebase that has established conventions using concatenation

For modern TypeScript projects, template literals should be your default choice. They're faster to write, easier to read, and less error-prone.

Improving Readability of Multiline Strings

Good formatting is important, especially with large text blocks or structured content like HTML or JSON. Keep proper indentation in your multiline strings to make them more readable.

const htmlString = `
<div>
<h1>Heading</h1>
<p>Paragraph of text.</p>
</div>
`;

console.log(htmlString);

The indentation in your template literal will be preserved in the output, making it easier to visualize the structure. This is particularly useful when working with string interpolation or when generating HTML templates for web applications.

You can also use this approach when working with Convex query functions that return formatted data:

const userTemplate = `
User: ${user.name}
Role: ${user.role}
Last Active: ${formatDate(user.lastActive)}
`;

Adding Variables within Multiline Strings

You can add variables to multiline strings using template literals by placing the variable name inside ${}.

const username = "Sarah Chen";
const greeting = `
Hello, ${username}!
Welcome to our application.
`;

console.log(greeting);

This feature, known as string interpolation, allows you to insert any valid TypeScript expression inside the ${} placeholder. You can include variables, function calls, or even complex expressions:

const items = ["apple", "banana", "orange"];
const itemList = `
You have ${items.length} items in your cart:
${items.map((item, index) => `${index + 1}. ${item}`).join('\n')}
`;

When building applications with Convex, you can leverage this technique to format data retrieved from your database before sending it to your frontend.

Managing Special Characters in Multiline Strings

Special characters like quotes, backslashes, and newlines can mess up formatting if not handled properly. Use backslashes to escape these characters.

const specialCharsString = `
This string contains a "quote" and a \\ backslash.
It also spans multiple lines with a newline at the end of this line.
${"\n"}And this text is on a new line.
`;

console.log(specialCharsString);

For explicit control over line breaks, you can use the \n escape sequence within expressions:

const formattedText = `First line${"\n"}Second line after an explicit break`;
console.log(formattedText);

This explicit control becomes useful when working with template literals in complex string manipulation scenarios, especially when building complex filters in Convex where string formatting is critical.

Joining Multiple Strings into One Multiline String

You can join multiple strings into one multiline string with the + operator or by using template literals.

const firstPart = "This is the first part of the string.";
const secondPart = `
This is the second part,
spanning multiple lines.
`;

const fullString = firstPart + "\n" + secondPart;
console.log(fullString);

Alternatively, using template literals for cleaner joining:

const fullStringTemplate = `${firstPart}
${secondPart}`;

console.log(fullStringTemplate);

Method 2 is generally preferred as it maintains readability. This technique is particularly useful when building complex text outputs in TypeScript applications or when working with Convex backend functions that need to format data from multiple sources.

You can also join arrays of strings into a multiline string using the join() method:

const paragraphs = [
"First paragraph",
"Second paragraph",
"Third paragraph"
];

const document = paragraphs.join("\n\n");

console.log(document);

Using Expressions Inside Multiline Strings

Expressions can be included in multiline strings by wrapping them in ${}.

const count = 5;
const listString = `
You have ${count} items in your list.
${count > 1 ? "They are:" : "It is:"}
Item 1
${count > 1 ? `Item 2` : ``}
`;

console.log(listString);

This example shows how to use conditional expressions within template literals to change text based on values, and demonstrates generating list items dynamically. You can even nest template literals for more complex structures:

const user = { name: "Alice", role: "Admin", tasks: ["Review", "Approve"] };
const userSummary = `
User: ${user.name}
Role: ${user.role}
Tasks: ${user.tasks.length > 0 ?
`\n${user.tasks.map(task => ` - ${task}`).join('\n')}` :
"None assigned"}
`;

console.log(userSummary);

This approach is valuable when building reactive UIs with Convex where you need to format and display information from the database without managing multiple state variables.

Keeping Indentation and Spacing in Multiline Strings

To keep indentation and spacing, make sure each line of your multiline string is aligned properly, especially when dealing with nested structures.

const nestedString = `
Outer level
Inner level
Deeply nested level
`;

console.log(nestedString);

For programmatically generated text with consistent indentation, use functions to handle the formatting:

function indent(text: string, spaces: number): string {
const padding = ' '.repeat(spaces);
return text.split('\n').map(line => line ? padding + line : line).join('\n');
}

const baseText = `Header
Content
Footer`;

const formattedText = indent(baseText, 4);
console.log(formattedText);

This technique works well when generating code or structured documents in TypeScript projects. When working with Convex database queries, you can format query results with proper indentation before displaying them.

For large JSON structures or complex HTML, controlling indentation makes debugging and maintaining the code more manageable:

const jsonTemplate = `{
"user": {
"name": "${userName}",
"permissions": [
${permissions.map(p => `"${p}"`).join(',\n ')}
]
}
}`;

Common Pitfalls & How to Avoid Them

Template literals are powerful, but a few common issues trip up developers. Here's what to watch for.

Unwanted Whitespace and Indentation

Template literals preserve every character, including spaces and tabs you use for code formatting. This can create unexpected output:

function generateMessage() {
const message = `
Hello!
Welcome to our app.
`;
return message;
}

console.log(generateMessage());
// Output includes leading spaces on each line:
//
// Hello!
// Welcome to our app.
//

If you want clean output without the indentation, you have a few options:

// Option 1: Start text at the left margin (breaks code formatting)
const message = `
Hello!
Welcome to our app.
`;

// Option 2: Split, trim, and rejoin
const message = `
Hello!
Welcome to our app.
`.split('\n').map(line => line.trim()).join('\n');

// Option 3: Use a library like 'common-tags' for complex cases
import { stripIndent } from 'common-tags';

const message = stripIndent`
Hello!
Welcome to our app.
`;

For most cases, being aware of the whitespace is enough. If you need precise formatting, the split/trim/join approach works well.

Performance with Complex Expressions

While template literals are fast, putting complex calculations inside them can slow things down if you're generating thousands of strings:

// Avoid this in performance-critical loops
const messages = items.map(item => `
Item: ${item.name}
Price: ${calculatePrice(item) * getTaxRate() + getShipping(item)}
Description: ${formatDescription(item.desc)}
`);

// Better: Pre-calculate values
const messages = items.map(item => {
const price = calculatePrice(item) * getTaxRate() + getShipping(item);
const description = formatDescription(item.desc);

return `
Item: ${item.name}
Price: ${price}
Description: ${description}
`;
});

For normal use, this isn't a concern. But if you're building a high-frequency rendering loop or processing large datasets, pre-calculating values before interpolation makes a difference.

Forgetting to Escape Backticks

If your string content includes a backtick character, you need to escape it with a backslash:

const codeExample = `Use backticks (\`) to create template literals`;
console.log(codeExample); // Use backticks (`) to create template literals

This is rare but causes syntax errors when it happens. The fix is simple once you know about it.

Accidentally Creating Multiline Strings

Sometimes you want a long string on one line in your code, but spread across multiple lines for readability. Template literals turn those line breaks into actual newlines:

// This creates a multiline string (probably not what you want)
const url = `https://api.example.com/users?
filter=active&
sort=name&
limit=100`;

// This is a single line with weird whitespace
console.log(url);

// Use backslash to escape newlines if you want a single line
const url = `https://api.example.com/users?\
filter=active&\
sort=name&\
limit=100`;

// Or just use concatenation for this case
const url = 'https://api.example.com/users?' +
'filter=active&' +
'sort=name&' +
'limit=100';

Understanding these edge cases helps you avoid debugging sessions where your strings look almost right but have subtle formatting issues.

Tagged Templates: Advanced String Processing

Tagged templates let you process template literals with a function, giving you control over how the string is constructed. While this is an advanced feature, a couple of practical use cases make it worth knowing about.

How Tagged Templates Work

You place a function name directly before the template literal. The function receives the string parts and interpolated values separately:

function simpleTag(strings: TemplateStringsArray, ...values: any[]) {
console.log('String parts:', strings);
console.log('Values:', values);
return 'processed';
}

const name = 'Alice';
const age = 30;
const result = simpleTag`User ${name} is ${age} years old`;

// String parts: ['User ', ' is ', ' years old']
// Values: ['Alice', 30]
// result: 'processed'

Practical Use Case: HTML Escaping

One of the most useful applications is automatically escaping HTML to prevent XSS attacks:

function escapeHTML(strings: TemplateStringsArray, ...values: any[]): string {
const escape = (str: string) =>
String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');

return strings.reduce((result, str, i) => {
const value = values[i] !== undefined ? escape(values[i]) : '';
return result + str + value;
}, '');
}

const userInput = '<script>alert("xss")</script>';
const safeHTML = escapeHTML`<div>User said: ${userInput}</div>`;

console.log(safeHTML);
// <div>User said: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</div>

This is safer than remembering to manually escape every variable you insert into HTML.

Practical Use Case: SQL Query Safety

Another common use is building SQL queries with automatic parameter escaping (though using a proper ORM is usually better):

function sql(strings: TemplateStringsArray, ...values: any[]) {
// This is simplified - real implementations use prepared statements
const escapedValues = values.map(val => {
if (typeof val === 'string') {
return `'${val.replace(/'/g, "''")}'`; // Basic SQL string escaping
}
return val;
});

return strings.reduce((query, str, i) => {
return query + str + (escapedValues[i] || '');
}, '');
}

const userId = 42;
const status = "active";
const query = sql`SELECT * FROM users WHERE id = ${userId} AND status = ${status}`;

console.log(query);
// SELECT * FROM users WHERE id = 42 AND status = 'active'

Tagged templates are powerful for creating domain-specific languages or adding safety layers to string construction. Libraries like styled-components and graphql-tag use this feature extensively. For most day-to-day TypeScript work, regular template literals are enough, but it's good to know this tool exists when you need it.

Quick Reference: Multiline String Patterns

Here's a quick reference for common multiline string scenarios in TypeScript:

TaskApproachExample
Basic multiline stringUse backticks`Line 1\nLine 2`
String with variablesUse ${} for interpolation`Hello ${name}`
Remove unwanted whitespaceSplit, trim, join.split('\n').map(s => s.trim()).join('\n')
Escape backticksUse backslash`Use \` for templates`
Single line, readable codeEscape newlines with \`Long text\`<br/>`continues here`
Conditional contentTernary in ${}`Items: ${count > 0 ? count : 'none'}`
Join array to multilineUse .join('\n')lines.join('\n')
HTML escapingTagged templateescapeHTMLString ${userInput}``

Final Thoughts on TypeScript Multiline Strings

Template literals have made working with complex text in TypeScript significantly easier. Whether you're building error messages, formatting API responses, generating HTML templates, or constructing configuration strings, template literals give you clean, readable syntax that matches how the final output will look.

The key advantages:

  • Variables and expressions embed naturally with ${}
  • Line breaks appear exactly as written
  • No escape character juggling
  • Complex nested structures stay readable

Start with basic template literals for your multiline needs. As you encounter edge cases like unwanted whitespace or need advanced processing, you now know the patterns to reach for. And remember: readability matters. If your string is getting complicated, breaking it into smaller pieces or using a tagged template might make your code clearer.

For more TypeScript tips, check out our guide on string interpolation.