How to Replace Substring in TypeScript: Complete Guide with Examples
Executive Summary
TypeScript developers perform substring replacements thousands of times daily, yet many rely on basic methods without knowing advanced techniques that boost efficiency.
This guide covers five practical techniques ranging from simple string methods to powerful regular expressions, along with critical edge case handling that prevents bugs in production code. Whether you’re building a text processor, sanitizing user input, or transforming data pipelines, understanding when and how to apply each method will make your TypeScript code more robust and performant.
Learn TypeScript on Udemy
Main Data Table
| Method | Use Case | Performance | Difficulty |
|---|---|---|---|
replace() |
Single substring replacement | O(n) time, excellent | Beginner |
replaceAll() |
Global substring replacement | O(n) time, very good | Beginner |
replace() with regex |
Pattern-based replacement | O(n+m) time, good | Intermediate |
split() + join() |
Complex transformations | O(n) time, good | Intermediate |
| Custom regex with callback | Dynamic replacements | O(n) time, moderate | Advanced |
Breakdown by Experience Level
Different TypeScript developers approach substring replacement with varying complexity levels. Beginners typically start with replace(), while intermediate developers leverage regex patterns. Advanced developers implement callback-based replacements for dynamic content generation.
Beginner Level (replace and replaceAll)
// Simple single replacement
const text = "Hello World";
const result = text.replace("World", "TypeScript");
console.log(result); // "Hello TypeScript"
// Replace all occurrences (ES2021+)
const text2 = "cat cat cat";
const result2 = text2.replaceAll("cat", "dog");
console.log(result2); // "dog dog dog"
Intermediate Level (Regex Patterns)
// Case-insensitive replacement
const text = "The Quick Brown Fox";
const result = text.replace(/fox/i, "Hound");
console.log(result); // "The Quick Brown Hound"
// Replace all with regex global flag
const text2 = "Hello 123 World 456";
const result2 = text2.replace(/\d+/g, "[NUM]");
console.log(result2); // "Hello [NUM] World [NUM]"
Advanced Level (Callback Functions)
// Dynamic replacement with callback
const text = "Item: apple, Item: banana, Item: cherry";
const result = text.replace(/Item: (\w+)/g, (match, item) => {
return `PRODUCT[${item.toUpperCase()}]`;
});
console.log(result);
// "PRODUCT[APPLE], PRODUCT[BANANA], PRODUCT[CHERRY]"
Comparison Section
| Technique | Syntax Simplicity | Flexibility | Best For |
|---|---|---|---|
replace() method |
Excellent (1 line) | Low | Single static replacements |
replaceAll() method |
Excellent (1 line) | Low | Global static replacements |
replace(/regex/g) |
Good (1-2 lines) | High | Pattern-based replacements |
split().join() |
Good (1 line) | Medium | String array transformations |
| Callback replacement | Fair (3-5 lines) | Very High | Complex dynamic logic |
Key Factors to Consider
1. Understanding String Immutability in TypeScript
Strings in TypeScript are immutable, meaning every replace operation creates a new string. This is crucial for performance when dealing with large texts or frequent replacements. Each operation allocates new memory, so chaining multiple replacements can impact performance. For instance, replacing ten different substrings in a large document will create eleven string objects in memory.
// Inefficient: creates multiple string copies
let text = "original text";
text = text.replace("original", "new");
text = text.replace("text", "content");
text = text.replace("content", "data");
// More efficient: single operation with regex
const text2 = "original text".replace(/original|text|content/g, (match) => {
const map: Record = {
"original": "new",
"text": "content",
"content": "data"
};
return map[match];
});
2. Edge Case Handling: Empty Strings and Null Values
The most common bug appears when developers forget to validate input before replacement. Empty strings, null values, and undefined will cause unexpected behavior. Always implement defensive checks before operations.
// Safe replacement function
function safeReplace(
input: string | null | undefined,
searchValue: string,
replaceValue: string
): string {
if (!input || !searchValue) {
return input ?? "";
}
return input.replace(searchValue, replaceValue);
}
// Usage
console.log(safeReplace(null, "test", "demo")); // ""
console.log(safeReplace("hello", "", "world")); // "hello"
3. Regular Expression Escaping for Special Characters
When using regex patterns, special characters like ., *, ?, [, and ] have special meanings. Failing to escape them causes unexpected matches. Always escape user-provided search strings when using regex.
// Dangerous: user input not escaped
const userInput = "price: $10.00";
const regex = new RegExp(userInput, "g"); // Incorrect!
// Safe: escape special regex characters
function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const safeRegex = new RegExp(escapeRegex(userInput), "g"); // Correct
const text = "Item costs price: $10.00 or price: $20.00";
console.log(text.replace(safeRegex, "[PRICE]"));
// "Item costs [PRICE] or [PRICE]"
4. Performance Implications of Global Replacements
The replaceAll() method (ES2021) is optimized for replacing all occurrences, but understanding alternatives matters for older environments. The split().join() pattern often performs better than regex with the global flag for simple string replacements, especially on large texts.
// Method 1: replaceAll() - most readable
const result1 = text.replaceAll("old", "new");
// Method 2: regex with global flag
const result2 = text.replace(/old/g, "new");
// Method 3: split/join - often fastest for simple strings
const result3 = text.split("old").join("new");
// For large texts, split/join is typically 15-25% faster
// because it avoids regex compilation overhead
5. Callback Functions for Context-Aware Replacements
Advanced use cases require accessing match context—position, captured groups, or surrounding text. Callback functions enable this, though they add complexity. The callback receives the full match plus captured groups, allowing sophisticated transformations impossible with static replacement strings.
// Replace with match position context
const text = "apple apple apple";
let count = 0;
const result = text.replace(/apple/g, (match) => {
count++;
return `apple${count}`;
});
console.log(result); // "apple1 apple2 apple3"
// Replace with captured groups
const phoneText = "Call (555) 123-4567 or (555) 987-6543";
const result2 = phoneText.replace(
/\((\d{3})\) ([\d-]+)/g,
(match, area, number) => {
return `[${area}] ${number}`;
}
);
console.log(result2);
// "Call [555] 123-4567 or [555] 987-6543"
Historical Trends and API Evolution
TypeScript’s substring replacement capabilities have evolved significantly. Prior to ES2021, developers relied on replace(/pattern/g) for global replacements. The introduction of replaceAll() in 2021 simplified the API, eliminating the need for regex when performing global string replacements. Modern TypeScript (4.4+) strongly encourages replaceAll() for string literals due to its clarity and performance characteristics.
The shift toward replaceAll() reflects a broader TypeScript philosophy: prefer explicit, readable code over powerful-but-cryptic regex patterns. However, regex patterns remain essential for pattern matching, character classes, and complex transformations. Current best practice suggests using replaceAll() for simple string replacement and regex for anything more complex.
Expert Tips
Tip 1: Use replaceAll() for Modern TypeScript Projects
If your project targets ES2021 or later, always prefer replaceAll() over regex global flags for substring replacement. It’s more readable, less error-prone, and performs as well or better. Reserve regex for pattern matching.
// Recommended
const text = "hello world hello";
const result = text.replaceAll("hello", "hi");
// Avoid unless you need regex features
const result2 = text.replace(/hello/g, "hi");
Tip 2: Validate Input and Handle Errors Gracefully
Wrap replacement logic in try-catch blocks when processing user input or external data. Malformed regex patterns or unexpected null values should fail gracefully, not crash your application.
// Production-ready replacement function
function replaceSubstring(
text: string | null | undefined,
search: string,
replace: string
): string {
try {
if (!text) return "";
if (!search) return text;
return text.replaceAll(search, replace);
} catch (error) {
console.error("Replacement error:", error);
return text ?? "";
}
}
Tip 3: Cache Compiled Regular Expressions
If you perform the same regex replacement repeatedly, compile the regex once and reuse it. Regex compilation is expensive; recreating it in loops significantly degrades performance.
// Inefficient: compiles regex in every iteration
const items = ["item1", "item2", "item3"];
const results = items.map(item =>
item.replace(/item/g, "product") // Regex compiled 3 times
);
// Efficient: compile once, reuse
const ITEM_REGEX = /item/g;
const results2 = items.map(item =>
item.replace(ITEM_REGEX, "product") // Regex compiled once
);
Tip 4: Use Named Capture Groups for Clarity
When working with complex patterns, named capture groups make code self-documenting and maintainable. They eliminate confusion about which group is which.
// Named capture groups (ES2018+)
const dateText = "2026-04-09";
const result = dateText.replace(
/(?\d{4})-(?\d{2})-(?\d{2})/,
(match, ...args) => {
const groups = args[args.length - 1]; // Last arg is groups object
return `${groups.day}/${groups.month}/${groups.year}`;
}
);
console.log(result); // "09/04/2026"
Tip 5: Consider Performance for Large-Scale Text Processing
For processing megabytes of text, profile your code. The split/join pattern often outperforms both replaceAll() and regex approaches, especially for repeated operations on fixed strings.
// Benchmark different methods on large text
const largeText = "a".repeat(1000000) + " test " + "a".repeat(1000000);
// Method 1: split/join (fastest for simple replacements)
console.time("split-join");
const r1 = largeText.split(" ").join("-");
console.timeEnd("split-join");
// Method 2: replaceAll()
console.time("replaceAll");
const r2 = largeText.replaceAll(" ", "-");
console.timeEnd("replaceAll");
// Method 3: regex
console.time("regex");
const r3 = largeText.replace(/ /g, "-");
console.timeEnd("regex");
FAQ Section
Q1: What’s the difference between replace() and replaceAll() in TypeScript?
replace() replaces only the first occurrence of a substring, while replaceAll() replaces all occurrences. The replace() method accepts either a string or regex; when given a string, it only replaces the first match. When given a regex with the global flag (/g), it replaces all matches. replaceAll(), introduced in ES2021, only accepts strings and always replaces all occurrences. For modern TypeScript projects, prefer replaceAll() when replacing literal strings because it’s clearer and avoids regex escaping issues.
Q2: How do I replace substrings while preserving case sensitivity?
TypeScript’s replace() and replaceAll() methods are case-sensitive by default. If you need case-insensitive replacement, use a regex with the case-insensitive flag (i): text.replace(/pattern/gi, "replacement"). For case-sensitive replacement (the default), simply use text.replaceAll("pattern", "replacement"). To preserve the original case of matched text while replacing, implement a callback function that checks the case of each match.
Q3: Can I replace multiple different substrings in one operation?
Yes. The most efficient approach uses a regex alternation pattern with a callback function. Create a regex combining all search terms with the pipe operator: /(term1|term2|term3)/g, then use a callback to map each match to its replacement. Alternatively, for simple replacements, chain multiple replaceAll() calls, though this creates multiple string copies in memory. For significant performance improvements on large texts, use the regex callback approach to modify the string only once.
Q4: What happens if I try to replace a substring that doesn’t exist?
If the substring doesn’t exist, both replace() and replaceAll() return the original string unchanged. No error is thrown. This makes these methods safe for defensive programming—you can attempt replacements without worrying about exceptions. The return value is always a string, even if no replacement occurs, so you can safely chain operations.
Q5: How do I escape special characters when the search string comes from user input?
User input containing special regex characters must be escaped before using it in a regex pattern. Create a utility function that escapes characters like . * + ? [ ] ( ) { } | \ by prefixing them with backslashes. The pattern is: str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"). Alternatively, avoid regex entirely by using replaceAll() with user input directly—it doesn’t interpret special characters, making it safer for user-provided search strings.
Conclusion
Replacing substrings in TypeScript is deceptively simple at first glance but offers surprising depth. For straightforward use cases, replaceAll() is your go-to method—it’s readable, performant, and safe from regex escaping pitfalls. When patterns emerge, regular expressions provide the flexibility needed for complex transformations. Always validate input, handle edge cases like null or empty strings, and profile performance-critical code paths.
The key takeaway: match the tool to the problem. Use replaceAll() for literal string replacement, regex for pattern matching, split/join for performance on large texts, and callbacks for context-aware transformations. Remember that strings are immutable in TypeScript, so each replacement creates a new string object—optimize accordingly when performing multiple replacements.
Start with the simplest approach that solves your problem, then optimize only if profiling reveals a bottleneck. This pragmatic strategy keeps your code maintainable while ensuring it meets performance requirements.
Learn TypeScript on Udemy
Related tool: Try our free calculator