Skip to main content

Regular Expressions in TypeScript

Your email validation regex just let through user@@example..com, and now you're debugging production issues at 2am. Or maybe you're trying to parse log files and your pattern matches everything except what you actually need. Regular expressions are powerful, but one misplaced character can turn them into a debugging nightmare.

This guide cuts through the complexity. You'll learn how to write regex patterns that actually work, understand the difference between test() and match(), and build validation logic that catches errors before they hit production. No fluff, just practical patterns you can use immediately.

Pattern Matching with TypeScript Regex

TypeScript works with JavaScript's RegExp object, giving you two ways to create patterns. Choose literal syntax for static patterns and the constructor for dynamic ones:

// Literal syntax (preferred for static patterns)
const emailPattern = /\w+@\w+\.\w+/;
const isValidEmail = emailPattern.test("user@example.com"); // true

// Constructor (useful when building patterns dynamically)
const userInput = "example";
const dynamicPattern = new RegExp(`\\b${userInput}\\b`, 'i');
console.log(dynamicPattern.test("This is an Example")); // true

TypeScript's type system recognizes RegExp as a built-in type, so you get autocomplete and type checking on regex methods. This catches errors like calling a non-existent method or passing wrong argument types.

You can also use regex to check if a string contains specific patterns, which is essential for form validation and data processing:

// Case-insensitive search using the 'i' flag
const logEntry = "ERROR: Database connection failed";
const hasError = /error/i.test(logEntry);
console.log(hasError); // true

// Check if string contains a pattern
const apiKey = "sk_live_abc123xyz";
const isLiveKey = /^sk_live_/.test(apiKey);
console.log(isLiveKey); // true

Creating Effective Regular Expressions

Building regex patterns is about understanding the building blocks. Here are the essential components you'll use constantly:

// Character classes
const hasDigit = /\d/; // Matches any digit (0-9)
const hasWord = /\w/; // Matches word characters (a-z, A-Z, 0-9, _)
const hasSpace = /\s/; // Matches whitespace (space, tab, newline)

// Quantifiers
const zipCode = /^\d{5}$/; // Exactly 5 digits
const zipCodePlus4 = /^\d{5}(-\d{4})?$/; // 5 digits, optionally followed by -1234
const username = /^[a-zA-Z0-9_]{3,20}$/; // 3 to 20 alphanumeric characters

// Boundaries
const exactWord = /\bhello\b/; // Matches "hello" as a complete word
const startsWith = /^https/; // String must start with "https"
const endsWith = /\.com$/; // String must end with ".com"

// Character sets and negation
const hexColor = /^#[0-9A-Fa-f]{6}$/; // Hex color code
const noNumbers = /^[^0-9]+$/; // String with no digits

Real-world example for validating slugs (URL-friendly strings):

const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

console.log(slugPattern.test("typescript-guide")); // true
console.log(slugPattern.test("TypeScript Guide")); // false (uppercase and spaces)
console.log(slugPattern.test("-invalid-slug-")); // false (leading/trailing hyphens)

When working with complex patterns, it's often helpful to use the TypeScript playground to test and refine your expressions before integrating them into your code. For deeper understanding of how TypeScript processes these patterns, you might find the process shown in Convex's API generation code spelunking quite illuminating.

Understanding Regex Flags

Flags modify how your regex pattern behaves. You add them after the closing slash in literal notation, or as the second argument in the RegExp constructor. Here are all the flags you'll use:

// 'i' flag: Case-insensitive matching
const caseInsensitive = /typescript/i;
console.log(caseInsensitive.test("TypeScript")); // true
console.log(caseInsensitive.test("TYPESCRIPT")); // true

// 'g' flag: Global search (find all matches, not just first)
const findAllDigits = /\d/g;
const text = "Order 123 shipped to 456 Main St";
console.log(text.match(findAllDigits)); // ["1", "2", "3", "4", "5", "6"]

// 'm' flag: Multiline mode (^ and $ match line boundaries)
const multilineText = `line1
line2
line3`;
const startsWithLine = /^line/gm;
console.log(multilineText.match(startsWithLine)); // ["line", "line", "line"]

// 's' flag: Dot matches newlines (dotAll mode)
const dotAll = /hello.world/s;
console.log(dotAll.test("hello\nworld")); // true

// 'u' flag: Unicode mode (proper handling of Unicode characters)
const emoji = /\p{Emoji}/u;
console.log(emoji.test("😀")); // true

// 'y' flag: Sticky mode (matches from lastIndex position only)
const sticky = /\d+/y;
const numbers = "123 456 789";
sticky.lastIndex = 4;
console.log(sticky.exec(numbers)); // ["456"]

Combining Flags

You can combine multiple flags to get the behavior you need:

// Global + case-insensitive: Find all occurrences regardless of case
const findAllErrors = /error/gi;
const logs = "Error on line 1. error on line 2. ERROR on line 3";
console.log(logs.match(findAllErrors)); // ["Error", "error", "ERROR"]

// Multiline + global: Process each line independently
const extractHeaders = /^#\s+(.+)$/gm;
const markdown = `# Title 1
Some text
# Title 2
More text`;
const headers = [...markdown.matchAll(extractHeaders)].map(m => m[1]);
console.log(headers); // ["Title 1", "Title 2"]

Using Flags with RegExp Constructor

When building patterns dynamically, pass flags as the second argument:

const searchTerm = "TypeScript";
const flags = "gi"; // Global, case-insensitive

const pattern = new RegExp(searchTerm, flags);
const article = "TypeScript is great. Learn typescript today!";
console.log(article.match(pattern)); // ["TypeScript", "typescript"]

String Validation with Regular Expressions

Validation is where regex really shines. Use these patterns to catch bad data before it causes problems:

interface ValidationResult {
isValid: boolean;
errors: string[];
}

function validateEmail(email: string): ValidationResult {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const errors: string[] = [];

if (!emailRegex.test(email)) {
errors.push("Invalid email format");
}
if (email.length > 254) {
errors.push("Email too long");
}

return {
isValid: errors.length === 0,
errors
};
}

console.log(validateEmail("user@example.com")); // { isValid: true, errors: [] }
console.log(validateEmail("invalid.email")); // { isValid: false, errors: [...] }

For more complex validation needs, you might use argument validation in Convex to ensure data consistency throughout your application.

Common Validation Patterns

Here are production-ready patterns for common validation scenarios:

// Strong password: 8+ chars, uppercase, lowercase, number, special char
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
console.log(strongPassword.test("MyPass123!")); // true

// URL with optional protocol
const urlRegex = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w.-]*)*\/?$/;
console.log(urlRegex.test("https://example.com/path")); // true
console.log(urlRegex.test("example.com")); // true

// Phone number (US format)
const phoneRegex = /^(\+1[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}$/;
console.log(phoneRegex.test("(555) 123-4567")); // true
console.log(phoneRegex.test("555-123-4567")); // true
console.log(phoneRegex.test("5551234567")); // true

// Credit card number (basic format check)
const creditCard = /^(\d{4}[-\s]?){3}\d{4}$/;
console.log(creditCard.test("1234 5678 9012 3456")); // true
console.log(creditCard.test("1234-5678-9012-3456")); // true

// IPv4 address
const ipv4 = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
console.log(ipv4.test("192.168.1.1")); // true
console.log(ipv4.test("256.1.1.1")); // false

When working with validation, combining regex with utility types can make your code more robust and maintainable.

Choosing the Right Method: test() vs match() vs exec()

Regex gives you multiple methods for pattern matching, and choosing the right one matters. Here's when to use each:

test(): Quick Boolean Checks

Use test() when you only need to know if a pattern exists. It's the fastest option because it stops at the first match:

const hasNumber = /\d/.test("Order #123");
console.log(hasNumber); // true

// Perfect for validation
function isValidUsername(username: string): boolean {
return /^[a-zA-Z0-9_]{3,20}$/.test(username);
}

console.log(isValidUsername("john_doe")); // true
console.log(isValidUsername("a")); // false (too short)

match(): Extract All Matches at Once

Use match() when you need the actual matched strings. Without the g flag, it returns the first match plus capture groups. With g, it returns all matches:

const text = "Prices: $19.99, $29.99, $39.99";

// Without 'g': Returns first match with capture groups
const firstPrice = text.match(/\$(\d+\.\d+)/);
console.log(firstPrice);
// ["$19.99", "19.99", index: 8, input: "...", groups: undefined]

// With 'g': Returns all matches (no capture groups)
const allPrices = text.match(/\$\d+\.\d+/g);
console.log(allPrices); // ["$19.99", "$29.99", "$39.99"]

// Use matchAll for all matches with capture groups
const detailedPrices = [...text.matchAll(/\$(\d+)\.(\d+)/g)];
detailedPrices.forEach(match => {
console.log(`Dollars: ${match[1]}, Cents: ${match[2]}`);
});

exec(): Iterate Through Matches

Use exec() when you need precise control over matching, especially with the g flag. It returns one match at a time and updates the regex's lastIndex:

const logPattern = /\[(\d{4}-\d{2}-\d{2})\] (\w+): (.+)/g;
const logs = `[2024-01-15] INFO: Server started
[2024-01-15] ERROR: Connection failed
[2024-01-15] WARN: High memory usage`;

let match;
const parsedLogs = [];

while ((match = logPattern.exec(logs)) !== null) {
parsedLogs.push({
date: match[1],
level: match[2],
message: match[3]
});
}

console.log(parsedLogs);
// [
// { date: "2024-01-15", level: "INFO", message: "Server started" },
// { date: "2024-01-15", level: "ERROR", message: "Connection failed" },
// { date: "2024-01-15", level: "WARN", message: "High memory usage" }
// ]

Performance Comparison

For simple existence checks on large strings, test() is fastest. For extracting multiple matches, match() with g flag is most convenient. Use exec() when you need to process matches one at a time or when memory is a concern:

const largeText = "..."; // Imagine a huge string

// Fast: Stops at first match
if (/error/i.test(largeText)) {
console.log("Contains error");
}

// Moderate: Finds all matches at once (memory intensive for many matches)
const allErrors = largeText.match(/error/gi);

// Memory efficient: Process one at a time
const errorPattern = /error/gi;
let errorMatch;
while ((errorMatch = errorPattern.exec(largeText)) !== null) {
// Process each match without storing all in memory
console.log(`Found at index: ${errorMatch.index}`);
}

Extracting Data with Regular Expressions

Capture groups let you pull out specific parts of a match. They're essential for parsing structured data:

// Extract email from text
const text = "Contact support@example.com for help";
const emailMatch = text.match(/([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/);

if (emailMatch) {
console.log(`Full email: ${emailMatch[0]}`); // support@example.com
console.log(`Username: ${emailMatch[1]}`); // support
console.log(`Domain: ${emailMatch[2]}`); // example.com
}

// Parse date formats
const dateString = "Event scheduled for 2024-03-15";
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const dateMatch = dateString.match(dateRegex);

if (dateMatch) {
const [fullMatch, year, month, day] = dateMatch;
const date = new Date(fullMatch);
console.log(`Year: ${year}, Month: ${month}, Day: ${day}`);
}

// Extract URL components
const url = "https://api.example.com:8080/v1/users?id=123";
const urlPattern = /^(https?):\/\/([^:\/\s]+)(?::(\d+))?(\/[^\s?]*)?(?:\?(.+))?$/;
const urlMatch = url.match(urlPattern);

if (urlMatch) {
const [, protocol, host, port, path, query] = urlMatch;
console.log({ protocol, host, port, path, query });
// { protocol: "https", host: "api.example.com", port: "8080",
// path: "/v1/users", query: "id=123" }
}

Named Capture Groups

Named groups make your code more readable by giving captures meaningful names:

const logEntry = "[2024-01-15 14:30:22] ERROR: Database connection failed";
const logPattern = /\[(?<date>\d{4}-\d{2}-\d{2}) (?<time>\d{2}:\d{2}:\d{2})\] (?<level>\w+): (?<message>.+)/;

const match = logEntry.match(logPattern);
if (match?.groups) {
const { date, time, level, message } = match.groups;
console.log({ date, time, level, message });
// { date: "2024-01-15", time: "14:30:22", level: "ERROR",
// message: "Database connection failed" }
}

Capture groups are particularly useful when working with complex string interpolation or when you need to extract multiple pieces of data from a single string.

String Replacement with TypeScript Regex

The replace() and replaceAll() methods let you transform strings based on patterns. Use replace() for single replacements or with the g flag for all matches:

// Basic replacement (first match only)
const text = "Hello world, world!";
const replaced = text.replace(/world/, "TypeScript");
console.log(replaced); // "Hello TypeScript, world!"

// Global replacement (all matches)
const allReplaced = text.replace(/world/g, "TypeScript");
console.log(allReplaced); // "Hello TypeScript, TypeScript!"

// Using replaceAll (ES2021+)
const replaceAll = text.replaceAll("world", "TypeScript");
console.log(replaceAll); // "Hello TypeScript, TypeScript!"

Using Capture Groups in Replacements

Reference captured groups in your replacement string using $1, $2, etc., or use a function for complex transformations:

// Reformat dates
const isoDate = "2024-03-15";
const usDate = isoDate.replace(/(\d{4})-(\d{2})-(\d{2})/, "$2/$3/$1");
console.log(usDate); // "03/15/2024"

// Replacement function for complex logic
const prices = "Item: $19.99, Item: $29.99";
const withTax = prices.replace(/\$(\d+\.\d+)/g, (match, price) => {
const priceWithTax = (parseFloat(price) * 1.1).toFixed(2);
return `$${priceWithTax}`;
});
console.log(withTax); // "Item: $21.99, Item: $32.99"

// Clean up whitespace
const messyText = " Too many spaces ";
const cleaned = messyText.replace(/\s+/g, " ").trim();
console.log(cleaned); // "Too many spaces"

// Sanitize user input
const userInput = "<script>alert('xss')</script> Hello";
const sanitized = userInput.replace(/<[^>]*>/g, "");
console.log(sanitized); // " Hello"

Named Capture Groups in Replacements

Use named groups for more readable replacements:

const phoneNumber = "555-123-4567";
const formatted = phoneNumber.replace(
/(?<area>\d{3})-(?<prefix>\d{3})-(?<line>\d{4})/,
"($<area>) $<prefix>-$<line>"
);
console.log(formatted); // "(555) 123-4567"

Advanced Regex Features

Lookahead and Lookbehind Assertions

Assertions let you match patterns only when they're followed by (lookahead) or preceded by (lookbehind) specific patterns, without including those patterns in the match:

// Positive lookahead: Match only if followed by pattern
const priceBeforeTax = /\$\d+(?= \+ tax)/;
console.log("$50 + tax".match(priceBeforeTax)); // ["$50"]
console.log("$50".match(priceBeforeTax)); // null

// Negative lookahead: Match only if NOT followed by pattern
const notDotCom = /\w+@\w+\.(?!com)\w+/;
console.log(notDotCom.test("user@example.org")); // true
console.log(notDotCom.test("user@example.com")); // false

// Positive lookbehind: Match only if preceded by pattern
const priceAfterDollar = /(?<=\$)\d+/;
console.log("Total: $100".match(priceAfterDollar)); // ["100"]

// Negative lookbehind: Match only if NOT preceded by pattern
const notPrefixed = /(?<!un)\w+able/;
console.log(notPrefixed.test("readable")); // true
console.log(notPrefixed.test("unreadable")); // false

// Complex password validation using multiple lookaheads
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
console.log(strongPassword.test("Weak1")); // false (too short, no special char)
console.log(strongPassword.test("Strong1Pass!")); // true

Non-Capturing Groups

Use (?:...) when you need grouping but don't want to capture the result:

// Without non-capturing: Creates unnecessary captures
const withCapture = /(\d{3})-(\d{3})-(\d{4})/;
const match1 = "555-123-4567".match(withCapture);
console.log(match1); // ["555-123-4567", "555", "123", "4567"]

// With non-capturing: Only captures what you need
const withoutCapture = /(?:\d{3})-(\d{3})-(\d{4})/;
const match2 = "555-123-4567".match(withoutCapture);
console.log(match2); // ["555-123-4567", "123", "4567"]

// Performance benefit for complex patterns
const imageUrl = /(?:https?:\/\/)?(?:www\.)?(\w+)\.com\/(\w+\.(?:jpg|png|gif))/;
const match3 = "https://www.example.com/photo.jpg".match(imageUrl);
console.log(match3); // ["https://www.example.com/photo.jpg", "example", "photo.jpg"]

For complex data processing with regex, consider using TypeScript filters in Convex to handle advanced query scenarios.

Regex Pattern Cheat Sheet

Here's a quick reference for common patterns:

PatternDescriptionExample
\dAny digit (0-9)/\d{3}/ matches "123"
\wWord character (a-z, A-Z, 0-9, _)/\w+/ matches "hello_world"
\sWhitespace (space, tab, newline)/\s+/ matches " "
\DNon-digit/\D+/ matches "abc"
\WNon-word character/\W+/ matches "!@#"
\SNon-whitespace/\S+/ matches "text"
.Any character (except newline)/h.t/ matches "hat", "hot"
^Start of string/line/^hello/ matches start
$End of string/line/world$/ matches end
*0 or more times/ab*c/ matches "ac", "abc", "abbc"
+1 or more times/ab+c/ matches "abc", "abbc" but not "ac"
?0 or 1 time (optional)/colou?r/ matches "color", "colour"
{n}Exactly n times/\d{3}/ matches "123"
{n,}n or more times/\d{2,}/ matches "12", "123", "1234"
{n,m}Between n and m times/\d{2,4}/ matches "12", "123", "1234"
[abc]Any character in set/[aeiou]/ matches vowels
[^abc]Any character NOT in set/[^0-9]/ matches non-digits
[a-z]Character range/[a-zA-Z]/ matches letters
|Alternation (OR)/cat|dog/ matches "cat" or "dog"
(...)Capture group/(ha)+/ captures "ha", "haha"
(?:...)Non-capturing group/(?:https?):\/\// groups without capturing
(?=...)Positive lookahead/\d+(?= dollars)/ matches numbers before " dollars"
(?!...)Negative lookahead/\d+(?! dollars)/ matches numbers NOT before " dollars"
\bWord boundary/\bcat\b/ matches "cat" but not "category"

Ready-to-Use Patterns

// Common validation patterns
const patterns = {
email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
url: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b/,
phone: /^(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/,
zipCode: /^\d{5}(-\d{4})?$/,
creditCard: /^(\d{4}[-\s]?){3}\d{4}$/,
hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
ipv4: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
date: /^\d{4}-\d{2}-\d{2}$/, // YYYY-MM-DD
time: /^([01]\d|2[0-3]):([0-5]\d)$/, // HH:MM (24-hour)
slug: /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
username: /^[a-zA-Z0-9_]{3,20}$/
};

// Test them
console.log(patterns.email.test("user@example.com")); // true
console.log(patterns.hexColor.test("#FF5733")); // true
console.log(patterns.phone.test("(555) 123-4567")); // true

Type Safety with Regex in TypeScript

TypeScript doesn't validate regex patterns at compile time, but you can add type safety to regex results and create validation functions with proper typing.

Typing Match Results

TypeScript infers the return type of regex methods, but you can be more specific:

interface EmailParts {
full: string;
username: string;
domain: string;
}

function parseEmail(email: string): EmailParts | null {
const match = email.match(/^([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/);

if (!match) return null;

return {
full: match[0],
username: match[1],
domain: match[2]
};
}

const result = parseEmail("user@example.com");
if (result) {
console.log(result.username); // Type-safe access
}

Branded Types for Validated Strings

Create branded types to ensure strings have been validated:

// Define branded types
type Email = string & { readonly __brand: "Email" };
type Username = string & { readonly __brand: "Username" };

function validateEmail(input: string): Email | null {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(input) ? (input as Email) : null;
}

function validateUsername(input: string): Username | null {
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
return usernameRegex.test(input) ? (input as Username) : null;
}

// Functions that require validated input
function sendEmail(to: Email, subject: string): void {
console.log(`Sending email to ${to}`);
}

// Usage
const userInput = "user@example.com";
const validEmail = validateEmail(userInput);

if (validEmail) {
sendEmail(validEmail, "Hello"); // Type-safe: guaranteed valid
}

// This won't compile:
// sendEmail("not-validated@example.com", "Hello"); // Error!

Template Literal Types as Regex Alternatives

For simple patterns, template literal types can provide compile-time validation:

// Type-level validation for hex colors
type HexDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F';
type HexColor = `#${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}${HexDigit}`;

const validColor: HexColor = "#FF5733"; // OK
// const invalidColor: HexColor = "#GG5733"; // Error at compile time

// URL patterns
type Protocol = 'http' | 'https';
type Domain = `${string}.${string}`;
type URL = `${Protocol}://${Domain}`;

const validUrl: URL = "https://example.com"; // OK
// const invalidUrl: URL = "ftp://example.com"; // Error at compile time

Common Challenges and Solutions

Handling Escape Sequences

Remember that backslashes need to be escaped in string literals:

// Incorrect - single backslash in string
const badRegex = new RegExp('\d+');

// Correct - escaped backslash in string
const goodRegex = new RegExp('\\d+');

// Alternative - use regex literal
const betterRegex = /\d+/;

Managing Multiline Text

When working with multiline text, use the appropriate flags:

const multilineText = `Line 1
Line 2
Line 3`;

// Without multiline flag
console.log(/^Line/.test(multilineText)); // true (matches only at start of input)

// With multiline flag
console.log(/^Line/m.test(multilineText.slice(7))); // true (matches at start of any line)

Handling Regex Errors

When working with regex, it's important to handle potential errors properly with try catch blocks:

function validateEmail(email: string): boolean {
try {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
} catch (error) {
// Handle and log the error
console.error('Error validating email:', error);

// Determine the specific [`error type`](/optimization/typescript-catch-error-type) for better error handling
if (error instanceof SyntaxError) {
console.error('Invalid regex syntax');
}

return false;
}
}

Optimizing Regex Performance

Poorly written regex can cause catastrophic backtracking, where the engine tries exponentially many match combinations:

// Dangerous: Can hang with long strings
const dangerous = /(a+)+b/;
// const test = "aaaaaaaaaaaaaaaaaaaaaaaac"; // This could freeze!

// Safe: Linear time complexity
const safe = /a+b/;

// Inefficient: Multiple quantifiers on same group
const inefficient = /(\w+)*@example\.com/;

// Efficient: Single quantifier
const efficient = /\w+@example\.com/;

// Performance tip: Be specific about what you match
const vague = /.+@.+\..+/; // Matches too much
const specific = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; // More efficient

Testing Regex Patterns

Always test your regex thoroughly, especially for edge cases:

function testRegexPatterns() {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

const testCases = [
{ input: "user@example.com", expected: true },
{ input: "user.name@example.co.uk", expected: true },
{ input: "user@", expected: false },
{ input: "@example.com", expected: false },
{ input: "user@@example.com", expected: false },
{ input: "user@example", expected: false }
];

testCases.forEach(({ input, expected }) => {
const result = emailRegex.test(input);
console.log(`${input}: ${result === expected ? '✓' : '✗'} (expected ${expected}, got ${result})`);
});
}

testRegexPatterns();

When working with complex patterns, consider using TypeScript's type system to validate data before applying regex.

Mastering Regex in TypeScript

Regular expressions are a powerful tool in your TypeScript arsenal. You've learned how to create patterns, use flags to modify behavior, choose between test(), match(), and exec() based on your needs, and extract data with capture groups.

The key to effective regex use is starting simple and building complexity only when needed. A readable pattern that's slightly less efficient is better than a cryptic optimization that nobody can maintain. Test your patterns thoroughly, watch for performance issues with complex quantifiers, and use TypeScript's type system to add safety around your regex validation logic.

For more guidance on string operations, see string interpolation and string contains. When building validation logic, explore utility types to make your code more robust.