Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/fajarnugraha37/ts-rex/llms.txt

Use this file to discover all available pages before exploring further.

.compile() is the terminal method that turns your builder chain into a typed execution object. Up to this point, every chained call has been building an AST and accumulating type state — no native RegExp is created until .compile() is called. The returned CompiledRegex object carries the fully resolved TCaptures and TFlags types from your chain, giving .exec() a precise return type that reflects every capture group you defined and every flag you set.

The CompiledRegex interface

export interface CompiledRegex<TCaptures, TFlags> {
  pattern: string;
  native: RegExp;
  exec: (str: string) => MatchResult<TCaptures, TFlags>;
}
pattern
string
required
The raw regex pattern string compiled from the builder’s AST, without flag letters. You can inspect this to verify what pattern was generated or to pass it to another regex engine.
native
RegExp
required
The native JavaScript RegExp instance created at compile time with the correct flags applied. This instance is exposed for inspection and interop. Do not use it for repeated execution — it retains lastIndex state between calls. Use .exec() instead, which creates a fresh instance on every call.
exec
(str: string) => MatchResult<TCaptures, TFlags>
required
The primary execution method. Accepts an input string and returns a MatchResult typed according to the captures and flags in your builder chain. Creates a fresh RegExp instance on every call to guarantee stateless execution.

Stateless execution guarantee

A well-known pitfall with native JavaScript global and sticky regexes is that RegExp.prototype.exec mutates lastIndex on the instance. If you call exec on the same instance twice, the second call resumes from where the first left off — a source of subtle bugs when regexes are stored as module-level constants. TS-Rex eliminates this entirely. On every call to .exec(), a fresh RegExp is instantiated from the stored pattern and flags string:
// From builder.ts — simplified
const exec = (str: string): MatchResult<TCaptures, TFlags> => {
  const instance = new RegExp(pattern, flags); // fresh every call
  // ...
};
This means .exec() is a pure function: the same input always produces the same output, regardless of how many times it has been called before.

MatchResult types

MatchResult is a discriminated union whose shape is determined by TFlags at compile time.
export type MatchResult<TCaptures, TFlags> =
  TFlags extends { global: true }
    ? IterableIterator<SingleMatch<TCaptures, TFlags>>
    : SingleMatch<TCaptures, TFlags> | FailedMatch<TCaptures, TFlags>;

Without the global flag

When global is not set, .exec() returns SingleMatch | FailedMatch. Use the isMatch discriminant to narrow:
const result = pattern.exec(input);

if (result.isMatch) {
  // result is SingleMatch<TCaptures, TFlags>
  console.log(result.match);       // string — the full matched text
  console.log(result.myCapture);   // string — a named capture
} else {
  // result is FailedMatch<TCaptures, TFlags>
  console.log(result.match);       // null
  console.log(result.myCapture);   // undefined
}

SingleMatch<TCaptures, TFlags>

export type SingleMatch<TCaptures, TFlags> =
  TCaptures &
  { isMatch: true; match: string } &
  (TFlags extends { hasIndices: true }
    ? { readonly indices: Record<keyof TCaptures, [number, number]> & { match: [number, number] } }
    : Record<string, never>);
isMatch
true
required
Always true on a successful match. Use this as the discriminant in if / switch narrowing.
match
string
required
The full matched substring.
[capture name]
string
required
One property per named capture group defined in the builder chain. All captures are string on SingleMatch — optionality from .optional(), .zeroOrMore(), or .or() is reflected as string | undefined on the corresponding keys.
indices
Record<keyof TCaptures, [number, number]> & { match: [number, number] }
Present only when .withIndices() was called. Contains [start, end] tuples for the full match (indices.match) and for each named capture group. Indices follow the same slice convention as String.prototype.slicestart is inclusive, end is exclusive.

FailedMatch<TCaptures, TFlags>

export type FailedMatch<TCaptures, TFlags> =
  { isMatch: false; match: null } &
  { [K in keyof TCaptures]: undefined } &
  (TFlags extends { hasIndices: true }
    ? { readonly indices: undefined }
    : Record<string, never>);
isMatch
false
required
Always false when the pattern did not match.
match
null
required
Always null on a failed match.
[capture name]
undefined
required
Every capture key is present but typed as undefined, allowing safe property access without narrowing first.

With the global flag

When .global() is set, MatchResult collapses to IterableIterator<SingleMatch<TCaptures, TFlags>>. There is no FailedMatch — an empty iterator means no matches were found.
// When TFlags extends { global: true }:
// MatchResult<TCaptures, TFlags> resolves to:
IterableIterator<SingleMatch<TCaptures, TFlags>>

Usage examples

Basic single match with narrowing

import { rx } from '@fajarnugraha37/ts-rex';

const pattern = rx()
  .startOfInput()
  .capture('firstName', rx().oneOrMore(rx().wordChar()))
  .whitespace()
  .capture('lastName', rx().oneOrMore(rx().wordChar()))
  .endOfInput()
  .compile();

const result = pattern.exec('John Doe');

if (result.isMatch) {
  console.log(result.firstName); // "John"
  console.log(result.lastName);  // "Doe"
  console.log(result.match);     // "John Doe"
} else {
  console.log(result.firstName); // undefined
}

Global iteration

import { rx } from '@fajarnugraha37/ts-rex';

const pattern = rx()
  .capture('word', rx().oneOrMore(rx().wordChar()))
  .global()
  .compile();

// exec() returns IterableIterator<SingleMatch> — iterate directly
for (const result of pattern.exec('hello world foo')) {
  console.log(result.word); // "hello", "world", "foo"
}

// Spread into an array
const matches = [...pattern.exec('hello world foo')];
console.log(matches.length); // 3

Match indices

import { rx } from '@fajarnugraha37/ts-rex';

const pattern = rx()
  .capture('key',   rx().oneOrMore(rx().wordChar()))
  .literal('=')
  .capture('value', rx().oneOrMore(rx().notWhitespace()))
  .withIndices()
  .compile();

const result = pattern.exec('lang=TypeScript');

if (result.isMatch) {
  console.log(result.key);            // "lang"
  console.log(result.value);          // "TypeScript"
  console.log(result.indices.match);  // [0, 15]
  console.log(result.indices.key);    // [0, 4]
  console.log(result.indices.value);  // [5, 15]
}

Inspecting pattern and native

import { rx } from '@fajarnugraha37/ts-rex';

const compiled = rx()
  .startOfInput()
  .capture('id', rx().oneOrMore(rx().digit()))
  .endOfInput()
  .compile();

console.log(compiled.pattern); // "^(?<id>(?:\d)+)$"
console.log(compiled.native);  // /^(?<id>(?:\d)+)$/

// Pass the pattern to another tool
const nativeCopy = new RegExp(compiled.pattern, 'g');
Using compiled.native directly for repeated matching is subject to the lastIndex mutation bug described above. Prefer compiled.exec() for all match operations.

Build docs developers (and LLMs) love