Everything you need to write regex like a normal person. No PhD required.
# npm
npm install zeroreg
# pnpm
pnpm add zeroreg
# yarn
yarn add zeroreg
# bun
bun add zeroregimport { digit, literal } from 'zeroreg'
// Match a phone number: 123-456-7890
const phone = digit(3).then('-').then(digit(3)).then('-').then(digit(4))
phone.test('123-456-7890') // true
phone.toRegex() // /\d{3}-\d{3}-\d{4}/import { email, url, phone } from 'zeroreg/patterns'
email.test('hello@world.com') // true
url.test('https://github.com') // true
phone.test('+1-234-567-8900') // truezeroReg is built on one simple idea: regex should read like English. Every function returns a Pattern object that you can chain, combine, and convert to native RegExp.
Every builder function returns a Pattern. Patterns are immutable and chainable:
const pattern = digit() // Create a pattern
.oneOrMore() // Chain quantifier
.then('-') // Chain more patterns
.then(letter().times(3)) // Nest patterns
// Convert to RegExp when ready
const regex = pattern.toRegex() // /\d+-[a-zA-Z]{3}/Match specific types of characters.
| Function | Description | Regex |
|---|---|---|
| digit() | Any digit 0-9 | \\d |
| digit(n) | Exactly n digits | \\d{n} |
| nonDigit() | Any non-digit | \\D |
| word() | Word char (a-z, 0-9, _) | \\w |
| nonWord() | Non-word char | \\W |
| letter() | Any letter a-z, A-Z | [a-zA-Z] |
| lowercase() | Lowercase letter | [a-z] |
| uppercase() | Uppercase letter | [A-Z] |
| alphanumeric() | Letter or digit | [a-zA-Z0-9] |
| whitespace() | Whitespace | \\s |
| nonWhitespace() | Non-whitespace | \\S |
| any() | Any character | . |
| literal(str) | Exact match (escaped) | str |
| charIn('abc') | Any char in set | [abc] |
| charNotIn('abc') | Any char NOT in set | [^abc] |
| range('a', 'z') | Character range | [a-z] |
import { digit, letter, charIn, literal } from 'zeroreg'
// Match exactly 3 digits
digit(3).test('123') // true
// Match a vowel
charIn('aeiou').test('e') // true
// Match a literal dot (escaped automatically)
literal('.').test('.') // true
literal('.').test('a') // falseControl how many times a pattern matches.
| Method | Description | Regex |
|---|---|---|
| .oneOrMore() | 1 or more | + |
| .zeroOrMore() | 0 or more | * |
| .optional() | 0 or 1 | ? |
| .times(n) | Exactly n times | {n} |
| .between(min, max) | Between min and max | {min,max} |
| .atLeast(n) | n or more times | {n,} |
| .atMost(n) | 0 to n times | {0,n} |
import { digit, letter, optional } from 'zeroreg'
// One or more digits
digit().oneOrMore().test('123') // true
// Optional plus sign, then digits
optional('+').then(digit().oneOrMore()).test('+123') // true
optional('+').then(digit().oneOrMore()).test('123') // true
// Between 2 and 4 letters
letter().between(2, 4).test('abc') // trueCapture parts of your match or group patterns together.
| Function | Description | Regex |
|---|---|---|
| capture(pattern) | Capturing group | (...) |
| capture(pattern, 'name') | Named capture | (?<name>...) |
| group(pattern) | Non-capturing group | (?:...) |
| oneOf(a, b, c) | Match any of | (?:a|b|c) |
import { capture, digit, oneOf, group } from 'zeroreg'
// Extract year, month, day
const datePattern = capture(digit(4), 'year')
.then('-')
.then(capture(digit(2), 'month'))
.then('-')
.then(capture(digit(2), 'day'))
const match = '2024-03-15'.match(datePattern.toRegex())
match.groups.year // '2024'
match.groups.month // '03'
match.groups.day // '15'
// Match "cat" or "dog" or "bird"
oneOf('cat', 'dog', 'bird').test('dog') // true
// Group without capturing (for quantifiers)
group(literal('ab').or('cd')).oneOrMore().test('abcdab') // trueMatch positions in the string, not characters.
| Function | Description | Regex |
|---|---|---|
| startOfLine() | Start of string | ^ |
| endOfLine() | End of string | $ |
| wordBoundary() | Word boundary | \\b |
| nonWordBoundary() | Non-word boundary | \\B |
import { startOfLine, endOfLine, literal, digit, wordBoundary } from 'zeroreg'
// Match "hello" at start of string
startOfLine().then(literal('hello')).test('hello world') // true
startOfLine().then(literal('hello')).test('say hello') // false
// Match exactly 3 digits (nothing more)
startOfLine().then(digit(3)).then(endOfLine()).test('123') // true
startOfLine().then(digit(3)).then(endOfLine()).test('1234') // false
// Match whole word "cat" (not "category")
wordBoundary().then(literal('cat')).then(wordBoundary())
.toRegex().test('the cat sat') // trueAssert what comes before or after without including it in the match.
| Function | Description | Regex |
|---|---|---|
| lookahead(pattern) | Followed by | (?=...) |
| negativeLookahead(pattern) | NOT followed by | (?!...) |
| lookbehind(pattern) | Preceded by | (?<=...) |
| negativeLookbehind(pattern) | NOT preceded by | (?<!...) |
import { digit, lookahead, lookbehind, literal } from 'zeroreg'
// Match digits followed by "px"
digit().oneOrMore().then(lookahead(literal('px')))
.toRegex().exec('100px') // ['100']
// Match digits preceded by "$"
lookbehind(literal('$')).then(digit().oneOrMore())
.toRegex().exec('$500') // ['500']The .then() method lets you chain patterns together sequentially.
import { digit, literal, letter, optional } from 'zeroreg'
// Build complex patterns step by step
const productCode = literal('PRD')
.then('-')
.then(digit(4))
.then('-')
.then(letter().times(2))
productCode.test('PRD-1234-AB') // true
productCode.toRegex() // /PRD-\d{4}-[a-zA-Z]{2}/
// .then() accepts strings (auto-escaped) or patterns
digit(3).then('.').then(digit(3)) // Matches "123.456"Convert your pattern to different formats and use it.
| Method | Returns | |
|---|---|---|
| .toRegex(flags?) | Native RegExp object | |
| .test(string) | boolean — does it match? | |
| .match(string) | First match or null | |
| .matchAll(string) | Array of all matches | |
| .replace(string, replacement) | String with replacements | |
| .toString() | Regex pattern string |
import { digit } from 'zeroreg'
const pattern = digit().oneOrMore()
// Test if string matches
pattern.test('abc123') // true
// Get first match
pattern.match('abc 123 def 456') // ['123']
// Get all matches
pattern.matchAll('abc 123 def 456') // [['123'], ['456']]
// Replace all matches
pattern.replace('abc 123 def 456', 'X') // 'abc X def X'
// Get native RegExp with flags
pattern.toRegex('gi') // /\d+/gi
// Get pattern string
pattern.toString() // '\d+'Import ready-to-use patterns for common use cases.
import { email, url, phone, date, uuid, ... } from 'zeroreg/patterns'emailuser@domain.com
urlhttps://example.com
phone+1-234-567-8900
date2024-03-15
time14:30:00
ipv4192.168.1.1
ipv62001:0db8:...
uuid550e8400-e29b-...
hexColor#ff6600
hexa1b2c3
slugmy-post-title
hashtag#trending
mention@username
creditCard4111111111111111
ssn123-45-6789
zipCode12345-6789
usernameuser_123
strongPasswordMyP@ssw0rd
semver1.2.3-alpha
macAddress00:1A:2B:3C:4D:5E
import { digit, optional } from 'zeroreg'
const phone = optional('+')
.then(digit(3))
.then('-')
.then(digit(3))
.then('-')
.then(digit(4))
phone.test('123-456-7890') // true
phone.test('+123-456-7890') // trueimport { digit, capture } from 'zeroreg'
const datePattern = capture(digit(4), 'year')
.then('-')
.then(capture(digit(2), 'month'))
.then('-')
.then(capture(digit(2), 'day'))
const match = '2024-03-15'.match(datePattern.toRegex())
console.log(match.groups)
// { year: '2024', month: '03', day: '15' }import { startOfLine, endOfLine, any, digit, letter, lookahead, charIn } from 'zeroreg'
const password = startOfLine()
.then(lookahead(any().zeroOrMore().then(digit()))) // has digit
.then(lookahead(any().zeroOrMore().then(letter()))) // has letter
.then(lookahead(any().zeroOrMore().then(charIn('@$!%*?&')))) // has special
.then(any().between(8, 32))
.then(endOfLine())
password.test('MyP@ssw0rd') // true
password.test('weakpass') // falseimport { literal, digit, lookbehind } from 'zeroreg'
// Match price amount after "$"
const price = lookbehind(literal('$'))
.then(digit().oneOrMore())
.then(literal('.').then(digit(2)).optional())
const matches = price.matchAll('Items: $10, $25.99, $100.00')
// ['10'], ['25.99'], ['100.00']Full TypeScript support out of the box. All functions are fully typed.
import { digit, capture, type Pattern } from 'zeroreg'
// Pattern type
const phone: Pattern = digit(3).then('-').then(digit(4))
// Named captures are type-safe
const date = capture(digit(4), 'year')
.then('-')
.then(capture(digit(2), 'month'))
const regex = date.toRegex()
const match = '2024-03'.match(regex)
if (match?.groups) {
const { year, month } = match.groups // TypeScript knows these exist
}