Named capture groups make regular expressions more readable and maintainable. Here's how to use them.
Basic Syntax#
1// Traditional numbered groups
2const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
3const match = dateRegex.exec('2024-01-15');
4console.log(match[1]); // '2024' - what is this?
5console.log(match[2]); // '01' - month or day?
6console.log(match[3]); // '15'
7
8// Named groups - much clearer
9const namedDateRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
10const namedMatch = namedDateRegex.exec('2024-01-15');
11
12console.log(namedMatch.groups.year); // '2024'
13console.log(namedMatch.groups.month); // '01'
14console.log(namedMatch.groups.day); // '15'String.match()#
1const pattern = /(?<protocol>https?):\/\/(?<domain>[^\/]+)(?<path>\/.*)?/;
2
3const url = 'https://example.com/path/to/page';
4const match = url.match(pattern);
5
6if (match) {
7 const { protocol, domain, path } = match.groups;
8 console.log(`Protocol: ${protocol}`); // 'https'
9 console.log(`Domain: ${domain}`); // 'example.com'
10 console.log(`Path: ${path}`); // '/path/to/page'
11}
12
13// Non-matching optional groups return undefined
14const simpleUrl = 'https://example.com';
15const simpleMatch = simpleUrl.match(pattern);
16console.log(simpleMatch.groups.path); // undefinedString.matchAll()#
1const text = 'Call 555-123-4567 or 555-987-6543 for info';
2const phonePattern = /(?<area>\d{3})-(?<exchange>\d{3})-(?<number>\d{4})/g;
3
4// Get all matches with groups
5for (const match of text.matchAll(phonePattern)) {
6 const { area, exchange, number } = match.groups;
7 console.log(`(${area}) ${exchange}-${number}`);
8}
9// (555) 123-4567
10// (555) 987-6543
11
12// Convert to array
13const phones = [...text.matchAll(phonePattern)]
14 .map(m => m.groups);
15
16console.log(phones);
17// [
18// { area: '555', exchange: '123', number: '4567' },
19// { area: '555', exchange: '987', number: '6543' }
20// ]String.replace()#
1// Reference named groups in replacement
2const dateStr = '2024-01-15';
3const pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
4
5// Using $<name> syntax
6const usFormat = dateStr.replace(pattern, '$<month>/$<day>/$<year>');
7console.log(usFormat); // '01/15/2024'
8
9// Using replacement function
10const formatted = dateStr.replace(
11 pattern,
12 (match, year, month, day, offset, string, groups) => {
13 return `${groups.month}/${groups.day}/${groups.year}`;
14 }
15);
16
17// Simpler with destructuring
18const formatted2 = dateStr.replace(pattern, (...args) => {
19 const { year, month, day } = args.at(-1); // groups is last arg
20 return `${month}/${day}/${year}`;
21});Backreferences#
1// Reference named group within same pattern using \k<name>
2const duplicateWords = /(?<word>\w+)\s+\k<word>/gi;
3
4const text = 'The the quick brown fox fox jumped';
5console.log(text.match(duplicateWords));
6// ['The the', 'fox fox']
7
8// Replace duplicates
9const fixed = text.replace(duplicateWords, '$<word>');
10console.log(fixed); // 'The quick brown fox jumped'
11
12// Combine with numbered backreferences
13const pattern = /(?<quote>['"]).*?\k<quote>/g;
14const str = `He said "hello" and 'goodbye'`;
15console.log(str.match(pattern));
16// ['"hello"', "'goodbye'"]Complex Patterns#
1// Email parsing
2const emailPattern = /(?<username>[a-zA-Z0-9._%+-]+)@(?<domain>[a-zA-Z0-9.-]+)\.(?<tld>[a-zA-Z]{2,})/;
3
4const email = 'user.name@example.com';
5const { username, domain, tld } = email.match(emailPattern).groups;
6console.log({ username, domain, tld });
7// { username: 'user.name', domain: 'example', tld: 'com' }
8
9// Time parsing
10const timePattern = /(?<hours>\d{1,2}):(?<minutes>\d{2})(?::(?<seconds>\d{2}))?(?<period>\s*[AP]M)?/i;
11
12const time1 = '14:30:45';
13const time2 = '2:30 PM';
14
15console.log(time1.match(timePattern).groups);
16// { hours: '14', minutes: '30', seconds: '45', period: undefined }
17
18console.log(time2.match(timePattern).groups);
19// { hours: '2', minutes: '30', seconds: undefined, period: ' PM' }Parsing Structured Data#
1// Log line parser
2const logPattern = /\[(?<timestamp>[^\]]+)\]\s+(?<level>\w+)\s+(?<message>.+)/;
3
4const logLine = '[2024-01-15 10:30:45] ERROR Database connection failed';
5const { timestamp, level, message } = logLine.match(logPattern).groups;
6
7console.log({ timestamp, level, message });
8// {
9// timestamp: '2024-01-15 10:30:45',
10// level: 'ERROR',
11// message: 'Database connection failed'
12// }
13
14// CSV-like parsing
15const csvPattern = /(?<name>[^,]+),(?<age>\d+),(?<city>[^,]+)/;
16const row = 'John Doe,30,New York';
17const data = row.match(csvPattern).groups;
18
19console.log(data);
20// { name: 'John Doe', age: '30', city: 'New York' }Optional Groups#
1// Handle optional parts gracefully
2const versionPattern = /v?(?<major>\d+)(?:\.(?<minor>\d+))?(?:\.(?<patch>\d+))?(?:-(?<prerelease>[a-z]+))?/i;
3
4function parseVersion(str) {
5 const match = str.match(versionPattern);
6 if (!match) return null;
7
8 return {
9 major: parseInt(match.groups.major),
10 minor: match.groups.minor ? parseInt(match.groups.minor) : 0,
11 patch: match.groups.patch ? parseInt(match.groups.patch) : 0,
12 prerelease: match.groups.prerelease || null,
13 };
14}
15
16console.log(parseVersion('v2.1.3')); // { major: 2, minor: 1, patch: 3, prerelease: null }
17console.log(parseVersion('1.0')); // { major: 1, minor: 0, patch: 0, prerelease: null }
18console.log(parseVersion('3.0.0-beta')); // { major: 3, minor: 0, patch: 0, prerelease: 'beta' }Validation Functions#
1// Password validation with named captures
2const passwordPattern = /^(?=.*(?<lowercase>[a-z]))(?=.*(?<uppercase>[A-Z]))(?=.*(?<digit>\d))(?=.*(?<special>[!@#$%^&*])).{8,}$/;
3
4function validatePassword(password) {
5 const match = password.match(passwordPattern);
6
7 if (!match) {
8 return {
9 valid: false,
10 missing: getMissingRequirements(password),
11 };
12 }
13
14 return { valid: true };
15}
16
17function getMissingRequirements(password) {
18 const missing = [];
19 if (!/[a-z]/.test(password)) missing.push('lowercase');
20 if (!/[A-Z]/.test(password)) missing.push('uppercase');
21 if (!/\d/.test(password)) missing.push('digit');
22 if (!/[!@#$%^&*]/.test(password)) missing.push('special');
23 if (password.length < 8) missing.push('length');
24 return missing;
25}Template Parsing#
1// Parse template variables
2const templatePattern = /\{\{(?<variable>\w+)(?:\|(?<filter>\w+))?\}\}/g;
3
4const template = 'Hello {{name|uppercase}}, your balance is {{balance}}';
5
6function parseTemplate(template, data, filters = {}) {
7 return template.replace(templatePattern, (match, ...args) => {
8 const { variable, filter } = args.at(-1);
9 let value = data[variable] ?? match;
10
11 if (filter && filters[filter]) {
12 value = filters[filter](value);
13 }
14
15 return value;
16 });
17}
18
19const result = parseTemplate(
20 template,
21 { name: 'John', balance: '$100' },
22 { uppercase: (s) => s.toUpperCase() }
23);
24
25console.log(result); // 'Hello JOHN, your balance is $100'RegExp.exec() Iteration#
1const pattern = /(?<tag>@\w+)/g;
2const text = 'Contact @support or @sales for help';
3
4// Using exec() in a loop
5let match;
6while ((match = pattern.exec(text)) !== null) {
7 console.log(`Found ${match.groups.tag} at index ${match.index}`);
8}
9// Found @support at index 8
10// Found @sales at index 20
11
12// Reset lastIndex for reuse
13pattern.lastIndex = 0;Best Practices#
Naming:
✓ Use descriptive names
✓ Follow camelCase
✓ Keep names short but clear
✓ Match domain terminology
Patterns:
✓ Group logically related parts
✓ Use non-capturing (?:) for structure
✓ Document complex patterns
✓ Test edge cases
Benefits:
✓ Self-documenting code
✓ Easier maintenance
✓ Safer refactoring
✓ Better error messages
Avoid:
✗ Overly long group names
✗ Mixing numbered and named
✗ Complex nested groups
✗ Forgetting undefined checks
Conclusion#
Named capture groups make regular expressions more readable and maintainable. Use the (?<name>...) syntax to create groups, access them via match.groups, and reference them in replacements with $<name> or backreferences with \k<name>. Always check for undefined when using optional groups.