Skip to main content

Using TypeScript string interpolation for flexible, cleaner code

You're building a user notification and your code looks like this: "Hello " + user.firstName + " " + user.lastName + ", you have " + unreadCount + " unread messages." It works, but it's hard to read, easy to mess up with missing spaces, and a pain to maintain. You've probably debugged issues where a missing + operator or misplaced quote broke the whole string.

String interpolation in TypeScript solves this. Using template literals with backticks, you can embed variables and expressions directly in your strings: `Hello ${user.firstName} ${user.lastName}, you have ${unreadCount} unread messages.` The result is cleaner, more readable code that's easier to maintain and less prone to bugs.

Introduction to Template Literals

Template literals use backticks (`) instead of quotes and let you embed expressions with ${}. They handle multiline strings naturally and make complex string building much simpler.

const apiKey = "sk_live_xyz123";
const endpoint = "users";
console.log(`https://api.example.com/${endpoint}?key=${apiKey}`);
// Outputs: https://api.example.com/users?key=sk_live_xyz123

When building applications with Convex and TypeScript, this feature becomes particularly useful for creating dynamic database queries and formatting output.

Syntax of String Interpolation

You can perform string interpolation with template literals using the ${} syntax, which lets you embed expressions directly into a string. This works with any valid TypeScript expression, from simple variables to complex calculations.

const requestCount = 42;
const requestLimit = 100;
const percentUsed = (requestCount / requestLimit) * 100;
console.log(`API usage: ${requestCount}/${requestLimit} requests (${percentUsed}% used)`);
// Outputs: API usage: 42/100 requests (42% used)

Differences between Single, Double, and Backtick Quotes

Single quotes ('') and double quotes ("") are for basic strings, while backticks (``) enable template literals with interpolation and multiline support.

const protocol = 'https';
const domain = "api.example.com";
const fullUrl = `${protocol}://${domain}/v1/users`; // Only backticks allow interpolation
console.log(fullUrl); // Outputs: https://api.example.com/v1/users

When working with custom functions in Convex, understanding these string differences helps you build more maintainable and readable backend code.

Embedding Expressions in Template Literals

You can embed function calls, calculations, and complex expressions directly within template literals. This is especially useful when building dynamic strings based on runtime data.

const formatCurrency = (amount: number) => `$${amount.toFixed(2)}`;
const calculateTax = (subtotal: number) => subtotal * 0.08;

const subtotal = 99.99;
console.log(`Subtotal: ${formatCurrency(subtotal)}, Tax: ${formatCurrency(calculateTax(subtotal))}`);
// Outputs: Subtotal: $99.99, Tax: $8.00

Dynamic strings are a key use case for template literals. When working with TypeScript in Convex, this pattern helps create more readable code for database queries and API responses.

Nesting Template Literals

Template literals can be nested to build strings from multiple dynamic parts. This is useful when you need to combine formatted components into a larger string.

const formatUser = (firstName: string, lastName: string) => `${firstName} ${lastName}`;
const formatTimestamp = (date: Date) => `${date.toLocaleDateString()} at ${date.toLocaleTimeString()}`;

const logEntry = (user: { firstName: string; lastName: string }, action: string) =>
`[${formatTimestamp(new Date())}] ${formatUser(user.firstName, user.lastName)} performed: ${action}`;

console.log(logEntry({ firstName: "Sarah", lastName: "Chen" }, "deleted file"));
// Outputs: [1/15/2025 at 2:30:45 PM] Sarah Chen performed: deleted file

Variables at different scopes can be referenced in these nested templates, creating flexible text generation. This approach resembles how Convex's functional relationships allow nesting data queries while maintaining type safety.

Tagged Template Literals

Tagged template literals let you process template literals with a custom function, giving you complete control over how interpolated values are formatted. This is useful for sanitization, formatting, or custom string processing.

function sanitizeHtml(strings: TemplateStringsArray, ...values: any[]) {
const escape = (str: string) => str.replace(/[<>&"']/g, (char) => {
const escapes: Record<string, string> = { '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#39;' };
return escapes[char];
});

return strings.reduce((result, str, i) =>
result + str + (i < values.length ? escape(String(values[i])) : ''), ''
);
}

const userInput = '<script>alert("xss")</script>';
const safeHtml = sanitizeHtml`<div>User said: ${userInput}</div>`;
console.log(safeHtml); // Outputs: <div>User said: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;</div>

When working with template literals in this advanced way, you gain capabilities similar to how Convex's argument validation processes and transforms input values while maintaining type safety.

Handling Multiline Strings

Template literals preserve line breaks and indentation, making them perfect for multiline text like SQL queries, HTML templates, or error messages. No more messy concatenation or escape characters.

const emailTemplate = `
Hello ${recipientName},

Thank you for your order #${orderId}.

Your items will ship within 2-3 business days.

Best regards,
The Team
`;

const sqlQuery = `
SELECT id, name, email
FROM users
WHERE created_at > '2025-01-01'
ORDER BY name ASC
`;

Dynamic String Construction

Template literals excel at building dynamic strings by embedding variables and expressions naturally. This reduces code complexity when working with data objects and runtime values.

const product = {
name: "Wireless Keyboard",
price: 79.99,
inStock: true,
quantity: 15
};

const productDescription = `${product.name} - $${product.price} (${product.inStock ? `${product.quantity} in stock` : 'Out of stock'})`;
console.log(productDescription); // Outputs: Wireless Keyboard - $79.99 (15 in stock)

Accessing object properties directly in template literals makes string formatting more intuitive. This pattern works well with Convex's TypeScript integrations when formatting database query results.

Conditional Expressions within Template Literals

You can include ternary operators and other conditional expressions directly in template literals. This creates dynamic strings that adapt to runtime conditions without needing separate variables.

const user = { name: "Alex", isPremium: true, credits: 150 };
const statusMessage = `${user.name} (${user.isPremium ? 'Premium Member' : 'Free Account'}) - ${user.credits} ${user.credits === 1 ? 'credit' : 'credits'} remaining`;
console.log(statusMessage); // Outputs: Alex (Premium Member) - 150 credits remaining

Formatting Numbers and Dates

Combine template literals with formatting methods to create readable number and date strings. This is cleaner than concatenating pre-formatted values.

const transaction = {
amount: 1234.56,
date: new Date('2025-01-15'),
currency: 'USD'
};

const receipt = `Transaction on ${transaction.date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric'
})}: ${transaction.amount.toLocaleString('en-US', {
style: 'currency',
currency: transaction.currency
})}`;

console.log(receipt); // Outputs: Transaction on January 15, 2025: $1,234.56

When to Use Template Literals vs Concatenation

While template literals are usually the better choice, knowing when to use each approach helps you write cleaner code.

Use template literals when:

  • Combining three or more values
  • Building URLs, queries, or formatted messages
  • Working with multiline strings
  • Embedding expressions or calculations
  • Readability matters (which is almost always)

Use concatenation when:

  • Joining just two simple strings where performance is absolutely critical
  • Working in codebases with older JavaScript standards
// Template literal - clear and maintainable
const apiUrl = `${protocol}://${domain}:${port}/${endpoint}?key=${apiKey}&limit=${limit}`;

// Concatenation - harder to read, easier to make mistakes
const apiUrlConcat = protocol + "://" + domain + ":" + port + "/" + endpoint + "?key=" + apiKey + "&limit=" + limit;

In practice, the performance difference between template literals and concatenation is negligible in modern JavaScript engines. Choose template literals for better readability and fewer bugs. The Convex Types Cookbook demonstrates similar principles of clarity and type safety when working with database operations.

Performance Considerations

Template literals have minimal performance overhead in modern JavaScript engines. The real performance issues come from what you put inside them, not the template literals themselves.

Potential performance concerns:

  • Complex calculations executed inside interpolations
  • Repeatedly building the same string in tight loops
  • Very large strings with many interpolations

Better approach:

// Avoid: Expensive calculation runs every time
const getUserLabel = (userId: number) =>
`User: ${expensiveDatabaseLookup(userId)}`;

// Better: Cache the result
const userName = expensiveDatabaseLookup(userId);
const getUserLabel = `User: ${userName}`;

// Avoid: Building strings in a hot loop
for (let i = 0; i < 10000; i++) {
const msg = `Processing item ${i} of ${total}`; // Creates 10,000 strings
}

// Better: Build once, or only when needed
const baseMsg = "Processing item";
for (let i = 0; i < 10000; i++) {
if (i % 1000 === 0) { // Only update on milestones
console.log(`${baseMsg} ${i} of ${total}`);
}
}

These considerations align with performance best practices seen in Convex applications, where managing computation efficiently improves overall application responsiveness.

Limitations of String Interpolation in TypeScript

TypeScript doesn't automatically handle certain advanced string operations in template literals, like:

  • No automatic handling of null/undefined values (unlike some other languages)

  • No built-in formatting directives for numbers or dates

  • No compile-time string validation without additional tools

const value: number | null = null;
console.log(`Value is ${value !== null ? value : "undefined"}`); // Outputs: Value is undefined

Type Inference with Template Literals

TypeScript can determine types from expressions in template literals, enhancing type safety and reducing errors. This type awareness helps catch errors early and enables IDE features like autocomplete for properties.

const num: number = 42;
console.log(`The number is ${num}`); // TypeScript infers 'number' type for 'num'

Template Literal Types

TypeScript extends template literals to the type system, letting you define string patterns as types. This provides compile-time validation for string formats, which is incredibly useful for API routes, CSS classes, and other structured strings.

// API route type safety
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIVersion = "v1" | "v2";
type APIRoute = `/api/${APIVersion}/${string}`;

const userRoute: APIRoute = "/api/v1/users"; // Valid
const productRoute: APIRoute = "/api/v2/products/123"; // Valid
// const invalid: APIRoute = "/users"; // Error: doesn't match pattern

Manipulating Union Types with Template Literals

Template literal types can combine union types to create all possible string combinations. This is powerful for generating type-safe variants like CSS classes, event names, or database field combinations.

// CSS utility class generation
type Size = "sm" | "md" | "lg";
type Variant = "primary" | "secondary" | "danger";
type ButtonClass = `btn-${Size}-${Variant}`;

const button: ButtonClass = "btn-md-primary"; // Valid
// const invalid: ButtonClass = "btn-xl-primary"; // Error: 'xl' not in Size

// ID prefixes (Stripe-style)
type IDPrefix = "cus" | "prod" | "inv";
type PrefixedID = `${IDPrefix}_${string}`;

const customerId: PrefixedID = "cus_abc123"; // Valid
const productId: PrefixedID = "prod_xyz789"; // Valid
// const invalid: PrefixedID = "abc123"; // Error: missing prefix

Using Enums in Template Literals

Template literals work with TypeScript enums to create type-safe strings. This prevents typos and ensures you're using valid enum values in your string construction.

enum LogLevel {
Info = "INFO",
Warning = "WARN",
Error = "ERROR"
}

enum Service {
Auth = "authentication",
Database = "database",
API = "api"
}

const createLogMessage = (level: LogLevel, service: Service, message: string) =>
`[${level}] ${service.toUpperCase()}: ${message}`;

console.log(createLogMessage(LogLevel.Error, Service.Database, "Connection timeout"));
// Outputs: [ERROR] DATABASE: Connection timeout

Enum values maintain their type information in template literals, ensuring you can't accidentally use invalid values. This type safety resembles how Convex's TypeScript integration prevents invalid database operations through type checking.

Template Literals with Generics

Combining template literals with generics<T> creates flexible, type-safe string utilities. This lets you build reusable formatting functions that preserve type information.

// Event name generator with type safety
function createEventName<T extends string>(category: T, action: string): `${T}:${string}` {
return `${category}:${action}`;
}

const userEvent = createEventName("user", "login"); // Type: "user:login"
const orderEvent = createEventName("order", "created"); // Type: "order:created"

// Typed environment variable names
function getEnvVar<T extends string>(prefix: T, key: string): `${T}_${string}` {
const envKey = `${prefix}_${key}` as `${T}_${string}`;
return envKey;
}

const apiKey = getEnvVar("API", "KEY"); // Type: "API_KEY"

Interpolating Values from Interfaces and Classes

Template literals make it easy to format structured data from interfaces and classes, creating consistent string output across your application.

interface APIResponse {
status: number;
message: string;
timestamp: Date;
}

class HttpError {
constructor(
public code: number,
public message: string,
public endpoint: string
) {}

toString() {
return `HTTP ${this.code} Error at ${this.endpoint}: ${this.message}`;
}
}

const response: APIResponse = {
status: 200,
message: "Success",
timestamp: new Date()
};

console.log(`[${response.status}] ${response.message} (${response.timestamp.toISOString()})`);
// Outputs: [200] Success (2025-01-15T14:30:00.000Z)

const error = new HttpError(404, "Resource not found", "/api/v1/users/999");
console.log(error.toString());
// Outputs: HTTP 404 Error at /api/v1/users/999: Resource not found

When working with structured data types, template literals help create consistent output formats. This approach pairs well with Convex's database schema design, where you can define structured document types and then format them for display.

Best Practices: Readability and Maintainability

Follow these guidelines to write clean, maintainable template literals:

Keep interpolations simple: Extract complex calculations to variables before embedding them.

// Avoid: Hard to read
const summary = `Total: $${(items.reduce((sum, item) => sum + item.price * item.qty, 0) * 1.08).toFixed(2)}`;

// Better: Clear and debuggable
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
const total = subtotal * 1.08;
const summary = `Total: $${total.toFixed(2)}`;

Use line breaks for long strings: Break template literals across multiple lines when they get long.

const notification = `
Hi ${user.name},

Your ${order.type} order #${order.id} has been ${order.status}.
Expected delivery: ${order.deliveryDate.toLocaleDateString()}
`;

Maintain consistent spacing: Be consistent with spaces around interpolated values for better readability.

Using template literals effectively aligns with broader code quality goals in TypeScript projects, where readability and maintainability are key concerns.

Security Considerations

Template literals don't provide any built-in protection against injection attacks. Always sanitize user input before embedding it, especially when the resulting strings are used in HTML, SQL queries, or shell commands.

Key risks to prevent:

  • Cross-site scripting (XSS) in web applications
  • SQL injection in database operations
  • Command injection in server-side code
// Dangerous: User input directly in HTML
const renderComment = (comment: string) => `<div>${comment}</div>`;
// If comment = "<script>alert('XSS')</script>", you have a security hole

// Safer: Sanitize user input
const escapeHtml = (unsafe: string) =>
unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");

const renderCommentSafe = (comment: string) => `<div>${escapeHtml(comment)}</div>`;

// Dangerous: SQL injection risk
const getUserQuery = (username: string) => `SELECT * FROM users WHERE name = '${username}'`;
// If username = "'; DROP TABLE users; --", you're in trouble

// Better: Use parameterized queries (example with a SQL library)
const getUserQuerySafe = (username: string) => ({
query: 'SELECT * FROM users WHERE name = ?',
params: [username]
});

This security awareness is particularly important when working with Convex's HTTP actions, where user inputs might flow directly into your application logic.

Troubleshooting Common Issues

Here's how to fix the most common problems you'll encounter with template literals.

Problem: Template literal not interpolating

// Wrong: Using quotes instead of backticks
const name = "Alex";
console.log('Hello, ${name}!'); // Outputs: Hello, ${name}! (not interpolated)

// Right: Use backticks
console.log(`Hello, ${name}!`); // Outputs: Hello, Alex!

Problem: Missing dollar sign

// Wrong: Forgot the $
console.log(`Result: {5 + 5}`); // Outputs: Result: {5 + 5}

// Right: Include the $
console.log(`Result: ${5 + 5}`); // Outputs: Result: 10

Problem: Undefined or null values

const user = { name: "Chris" }; // no 'age' property
console.log(`Age: ${user.age}`); // Outputs: Age: undefined

// Better: Handle missing values
console.log(`Age: ${user.age ?? 'Not specified'}`); // Outputs: Age: Not specified

Problem: Unexpected output from expressions

const items = [1, 2, 3];
console.log(`Items: ${items}`); // Outputs: Items: 1,2,3 (array toString)

// Better: Format arrays explicitly
console.log(`Items: ${items.join(', ')}`); // Outputs: Items: 1, 2, 3
console.log(`Count: ${items.length} items`); // Outputs: Count: 3 items

Debugging strategy:

  1. Break complex template literals into smaller parts to isolate issues
  2. Use console.log() to inspect interpolated values separately
  3. Check for misplaced or missing backticks (syntax highlighting helps)
  4. Verify object properties exist before accessing them

Understanding these common pitfalls aligns with the best practices for TypeScript in Convex, which helps developers avoid typical errors.

Handling Escape Characters

Template literals handle most special characters naturally, but they still respect JavaScript escape sequences like \n (newline) and \t (tab).

// Escape sequences work in template literals
const formatted = `Line 1\nLine 2\tTabbed`;
console.log(formatted);
// Outputs:
// Line 1
// Line 2 Tabbed

// Windows file paths are easier (but backslashes still need escaping)
const path = `C:\\Program Files\\TypeScript`;
console.log(path); // Outputs: C:\Program Files\TypeScript

// To include a literal backtick, escape it
const codeExample = `Use backticks: \`like this\` for template literals`;
console.log(codeExample); // Outputs: Use backticks: `like this` for template literals

// To include ${, escape the dollar sign
const literalInterpolation = `The syntax is \${variable}`;
console.log(literalInterpolation); // Outputs: The syntax is ${variable}

Quick Reference: Common Patterns

Here are the most useful template literal patterns you'll reach for regularly:

// Basic interpolation
const greeting = `Hello, ${userName}!`;

// Object property access
const userCard = `${user.firstName} ${user.lastName} (${user.email})`;

// Calculations and expressions
const summary = `${completed}/${total} tasks (${Math.round((completed / total) * 100)}%)`;

// Conditional (ternary) expressions
const status = `Status: ${isActive ? 'Active' : 'Inactive'}`;
const label = `${count} ${count === 1 ? 'item' : 'items'}`;

// Function calls
const timestamp = `Last updated: ${new Date().toLocaleString()}`;
const formatted = `Price: ${amount.toFixed(2)}`;

// Nullish coalescing for defaults
const display = `Name: ${userName ?? 'Anonymous'}`;

// Multiline strings
const email = `
Hi ${recipientName},

Your order ${orderId} has shipped.

Thanks!
`;

// URL building
const apiUrl = `${baseUrl}/api/${version}/${endpoint}?token=${token}`;

// Template literal types (type-level)
type EventName = `${string}:${string}`; // e.g., "user:login"
type APIPath = `/api/${string}`; // e.g., "/api/users"

Final Thoughts

Template literals have transformed string handling in TypeScript, making your code more readable and easier to maintain. They simplify previously tedious tasks like combining variables with text, formatting multiline strings, and creating dynamic content.

By mastering template literals, you'll write cleaner code that's less prone to errors. Whether you're building API URLs, formatting user messages, or creating type-safe string patterns, template literals give you the tools to handle strings effectively in your TypeScript projects.