Skip to main content

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.

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() or test() 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:

MethodReturnsBest ForPerformance
includes()booleanSimple yes/no checks for literal stringsFast
indexOf()numberFinding position, slicing, extracting substringsFast
search()numberPattern matching with regexSlower (only use when regex needed)
startsWith()booleanValidating prefixes (URLs, paths)Very fast
endsWith()booleanValidating suffixes (file extensions)Very fast
match()array or nullExtracting multiple matches or capture groupsSlower (regex)
test()booleanQuick regex pattern validationModerate

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) or match() (for extraction)
  • Need multiple values? → Array's some() or every() with includes()

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.