TypeScript String Contains
You're parsing user input and need to check if it contains profanity before saving it to your database. Or maybe you're validating URLs to ensure they use HTTPS. These are the everyday scenarios where string containment checks become essential. In this guide, we'll cover practical methods for checking if a TypeScript string contains a substring, when to use each approach, and how to handle tricky cases like case sensitivity and special characters.
Checking for Substrings in TypeScript
The includes method is the most straightforward way to check if a string contains another string. It returns a boolean, making your intent immediately clear:
const url = "https://api.example.com/users";
const isSecure = url.includes("https://");
console.log(isSecure); // Output: true
Searching from a Specific Position
You can tell includes where to start searching by passing a second parameter:
const logMessage = "INFO: User login successful. INFO: Session started.";
// Check if "INFO:" appears after position 10
console.log(logMessage.includes("INFO:", 10)); // Output: true
This is useful when you want to skip known prefixes or search only in specific parts of a string. When working with TypeScript filters, you might need to check if a string field contains a specific substring as part of your query conditions.
When You Need the Position: Using indexOf()
Sometimes you don't just need to know if a substring exists, but where it is. That's when indexOf() comes in handy:
const email = "user@example.com";
const atPosition = email.indexOf("@");
if (atPosition !== -1) {
const username = email.slice(0, atPosition);
console.log(`Username: ${username}`); // Output: Username: user
}
The indexOf method returns the position of the first occurrence of the substring or -1 if the substring isn't found. This makes it perfect for string parsing and extraction tasks.
function extractDomain(email: string): string | null {
const atIndex = email.indexOf("@");
if (atIndex === -1) {
return null; // Invalid email
}
return email.slice(atIndex + 1);
}
console.log(extractDomain("admin@company.com")); // Output: company.com
Use indexOf() when you need to slice, extract, or manipulate parts of a string based on where a substring appears.
Pattern Matching with search()
When you need to search using patterns instead of literal strings, the search() method accepts regular expressions:
const filename = "report_2024_final_v2.pdf";
// Find any 4-digit year
const yearPosition = filename.search(/\d{4}/);
console.log(yearPosition); // Output: 7
// Check if filename contains "final" or "draft"
const statusPosition = filename.search(/final|draft/);
console.log(statusPosition !== -1); // Output: true
The search() method returns the position of the first match (like indexOf), or -1 if nothing matches. However, there's an important performance consideration: use indexOf() for literal string searches and reserve search() for when you actually need regex pattern matching. The indexOf() method is significantly faster for simple string matching.
// For literal strings, prefer indexOf
const text = "Error: Connection timeout";
const hasError = text.indexOf("Error") !== -1; // Fast
// Use search() only when you need patterns
const logLine = "2024-01-15 ERROR: Database connection failed";
const hasErrorLevel = /ERROR|WARN|INFO/.test(logLine); // Justified use of regex
Using proper TypeScript functions with appropriate type annotations ensures that your code is type-safe and easier to maintain.
Checking Multiple Substrings at Once
In real applications, you'll often need to check if a string contains any of several possible substrings. Here's how to handle that efficiently:
const userComment = "This product is awesome and fantastic!";
const positiveWords = ["great", "awesome", "excellent", "fantastic"];
const isPositive = positiveWords.some(word => userComment.includes(word));
console.log(isPositive); // Output: true
This technique is useful when validating input against multiple potential values. For checking if a string contains all of several substrings, use every():
function isValidUrl(url: string): boolean {
const requiredParts = ["https://", "api.", ".com"];
return requiredParts.every(part => url.includes(part));
}
console.log(isValidUrl("https://api.example.com/data")); // Output: true
console.log(isValidUrl("http://example.com/data")); // Output: false
When building applications with TypeScript and Convex, these string operations become even more powerful when combined with database queries and filters.
Common Challenges and Solutions
When working with TypeScript strings, you'll encounter several practical challenges. Here are solutions to the most common issues:
Handling Case Sensitivity
The includes method is case-sensitive by default, which can trip you up:
const userInput = "Hello, World!";
console.log(userInput.includes("world")); // Output: false (case-sensitive)
For a case-insensitive search, normalize both strings first:
function containsIgnoreCase(text: string, searchTerm: string): boolean {
return text.toLowerCase().includes(searchTerm.toLowerCase());
}
const browserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
console.log(containsIgnoreCase(browserAgent, "WINDOWS")); // Output: true
This approach uses string methods to normalize case before comparison, ensuring more flexible matching.
For internationalized text where accents and special characters matter, you can use localeCompare() with specific options:
function containsLocaleInsensitive(text: string, searchTerm: string): boolean {
return text.toLowerCase().includes(searchTerm.toLowerCase());
}
// For more advanced locale-aware comparisons
function equalsCaseInsensitive(str1: string, str2: string): boolean {
return str1.localeCompare(str2, undefined, { sensitivity: 'base' }) === 0;
}
console.log(equalsCaseInsensitive("Café", "cafe")); // Handles accents
Working with Special Characters and Regular Expressions
When searching for strings with special characters or complex patterns, you'll need regex:
const text = "Contact us at support@example.com or call (555) 123-4567";
// Check if text contains a phone number pattern
const hasPhone = /\(\d{3}\)\s\d{3}-\d{4}/.test(text);
console.log(hasPhone); // Output: true
// Check if text contains an email
const hasEmail = /@\w+\.\w+/.test(text);
console.log(hasEmail); // Output: true
Regular expressions provide more powerful pattern matching capabilities than simple substring checks. When working with Convex server functions, you can use these same techniques for validating input data.
Choosing the Right Method for Performance
The method you choose can significantly impact performance, especially with large strings or repeated checks:
// For simple literal strings: use includes() or indexOf()
function filterLogs(logs: string[], level: string): string[] {
return logs.filter(log => log.includes(level)); // Fast for literals
}
// For pattern matching: use test() with regex (but only when needed)
function containsSensitiveData(text: string): boolean {
// Regex is justified here because we need pattern matching
const patterns = [
/\d{3}-\d{2}-\d{4}/, // SSN
/\d{16}/, // Credit card
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i // Email
];
return patterns.some(pattern => pattern.test(text));
}
Rules of thumb:
- Use
includes()when you just need a yes/no answer for a literal string - Use
indexOf()when you need the position for slicing or extraction - Use
search()ortest()only when you need regex pattern matching - Avoid converting to lowercase unless case-insensitivity is required (it creates a new string)
In a Convex backend, you can use complex filters to efficiently search for strings in your database documents.
Checking String Boundaries with startsWith() and endsWith()
Sometimes you don't just need to know if a substring exists, but whether it appears at the beginning or end of a string. That's where startsWith() and endsWith() shine:
function validateFilename(filename: string): boolean {
const allowedExtensions = [".jpg", ".png", ".gif"];
return allowedExtensions.some(ext => filename.endsWith(ext));
}
console.log(validateFilename("photo.jpg")); // Output: true
console.log(validateFilename("document.pdf")); // Output: false
These methods are especially useful for URL and path validation:
function isSecureApiCall(url: string): boolean {
return url.startsWith("https://") && url.includes("api.");
}
function isImagePath(path: string): boolean {
return path.startsWith("/images/") || path.startsWith("/uploads/");
}
console.log(isSecureApiCall("https://api.example.com/users")); // Output: true
console.log(isImagePath("/images/avatar.png")); // Output: true
You might use them when implementing string interpolation to build dynamic paths.
Extracting Matches with match()
When you need to not just check if a pattern exists, but extract the actual matches, use match():
const logMessage = "User 12345 logged in at 14:30:45";
// Extract all numbers
const numbers = logMessage.match(/\d+/g);
console.log(numbers); // Output: ["12345", "14", "30", "45"]
// Extract specific pattern
const userId = logMessage.match(/User (\d+)/);
if (userId) {
console.log(`User ID: ${userId[1]}`); // Output: User ID: 12345
}
The match() method returns an array of matches or null if nothing matches. Use the g flag for all matches, or omit it to get just the first match with capture groups. This is similar to how you might use filter operations in TypeScript arrays.
function extractHashtags(text: string): string[] {
const matches = text.match(/#\w+/g);
return matches || [];
}
const tweet = "Loving #TypeScript and #WebDev today!";
console.log(extractHashtags(tweet)); // Output: ["#TypeScript", "#WebDev"]
Quick Reference: When to Use Each Method
Here's a comparison table to help you choose the right method for your needs:
| Method | Returns | Best For | Performance |
|---|---|---|---|
includes() | boolean | Simple yes/no checks for literal strings | Fast |
indexOf() | number | Finding position, slicing, extracting substrings | Fast |
search() | number | Pattern matching with regex | Slower (only use when regex needed) |
startsWith() | boolean | Validating prefixes (URLs, paths) | Very fast |
endsWith() | boolean | Validating suffixes (file extensions) | Very fast |
match() | array or null | Extracting multiple matches or capture groups | Slower (regex) |
test() | boolean | Quick regex pattern validation | Moderate |
Quick decision guide:
- Need a boolean for a literal string? →
includes() - Need the position? →
indexOf() - Checking prefix/suffix? →
startsWith()/endsWith() - Need pattern matching? →
test()(for boolean) ormatch()(for extraction) - Need multiple values? → Array's
some()orevery()withincludes()
Final Thoughts on TypeScript String Contains
Use includes() for most straightforward substring checks—it's readable and fast. When you need the position for slicing or extraction, reach for indexOf(). Save regex methods like search(), test(), and match() for when you actually need pattern matching, since they're slower than literal string methods. For case-insensitive searches, normalize with toLowerCase() first. By picking the right method for each situation, you'll write TypeScript code that's both performant and easy to understand.