regex-wand
v0.4.3
Published
Typed regular expressions without giving up readable regex authoring.
Maintainers
Readme
regex-wand
Typed regular expressions without giving up readable regex authoring.
regex-wand lets you write patterns with the composable magic-regexp API and
get ArkRegex-powered TypeScript inference on the final RegExp.
At runtime, the result is still a native RegExp. The extra value is the typed
surface: .infer, .inferCaptures, .inferNamedCaptures, typed exec()
groups, literal flags, and test() narrowing. ArkRegex is type-only in the
built JavaScript.
Try It
- npm package
- Playground
- Monorepo README
- Type-safety guide
- ArkType interop
- Support matrix
- Roadmap
- Testing strategy
- Changelog
- GitHub releases
Install
bun add regex-wandnpm install regex-wandmagic-regexp and arkregex are installed with regex-wand. They remain part
of the public type surface because Magic Regex primitives are re-exported and
ArkRegex powers the inferred RegExp types.
Quick Start
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`
const match = route.exec("/users/42")
match?.groups.userId satisfies string | undefined
declare const value: string
if (route.test(value)) {
value satisfies `/users/${number}`
}Common Patterns
Exact semver-style match:
import { defineRegex, digit } from "regex-wand"
const semver = defineRegex({
match: "exact",
inputs: [
digit.times.any().grouped(),
".",
digit.times.any().grouped(),
".",
digit.times.any().grouped(),
],
})
semver.infer satisfies `${number}.${number}.${number}`
semver.inferCaptures satisfies [`${number}`, `${number}`, `${number}`]Contains-style text search:
import { defineRegex, digit } from "regex-wand"
const ticketId = defineRegex({
inputs: ["id:", digit.times.atLeast(1).grouped()],
})
ticketId.infer satisfies `${string}id:${number}${string}`
ticketId.test("ticket id:8042 is ready")Flags:
import {
caseInsensitive,
defineRegex,
global,
} from "regex-wand"
const accepted = defineRegex({
flags: [global, caseInsensitive],
match: "exact",
inputs: ["ok"],
})
accepted.flags satisfies "gi"
accepted.infer satisfies "ok" | "oK" | "Ok" | "OK"Why Not Just Magic Regex Or ArkRegex?
regex-wand exists because Magic Regex and ArkRegex solve different parts of
the problem.
Raw regex strings are compact but hard to safely compose:
const route = /^\/users\/(?<userId>\d{1,})$/Raw Magic Regex gives you readable, composable authoring:
import { createRegExp, digit } from "magic-regexp"
const route = createRegExp("/users/", digit.times.atLeast(1).as("userId"))
route.test("/users/42")Magic Regex also ships a build-time transform that can compile those
createRegExp(...) calls to plain RegExp literals.
Raw ArkRegex gives you strong result types from a raw regex string:
import { regex } from "arkregex"
const route = regex("^/users/(?<userId>\\d{1,})$")
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`regex-wand keeps the Magic Regex authoring model, adds readable object params,
and returns the ArkRegex-type surface:
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})
route.infer satisfies `/users/${number}`
route.inferNamedCaptures.userId satisfies `${number}`
route.test("/users/42")Use raw magic-regexp when you only need composable regex construction. Use raw
arkregex when you already have a regex string and want type inference for it.
Use regex-wand when you want both authoring ergonomics and result inference.
For new code, prefer defineRegex({ inputs, match, flags }). It is more
readable than ordered arguments once definitions need matching mode or flags.
For migration or Magic Regex-style authoring, createRegExp(...inputs) mirrors
Magic Regex's positional input API and returns the richer regex-wand result.
Build-Time Transform Note
Magic Regex's transform only recognizes imports from magic-regexp and
magic-regexp/further-magic. If you do not enable regex-wand/transform, direct
regex-wand builders are small runtime adapters:
import { defineRegex, digit } from "regex-wand"
const route = defineRegex({
match: "exact",
inputs: ["/users/", digit.times.atLeast(1).as("userId")],
})If you want Magic Regex's build-time transform in an app, compose with raw Magic Regex and adapt at the boundary:
import { createRegExp, digit, exactly } from "magic-regexp"
import { fromMagic } from "regex-wand"
const magicRoute = createRegExp(
exactly("/users/", digit.times.atLeast(1).as("userId"))
.at.lineStart()
.at.lineEnd(),
)
const route = fromMagic(magicRoute)
route.inferNamedCaptures.userId satisfies `${number}`With Magic Regex's transform enabled, the raw createRegExp(...) call can
compile away, leaving only the regex-wand boundary adapter. ArkRegex remains
type-only in regex-wand's built JavaScript.
If you want direct regex-wand builder calls to compile, use
regex-wand/transform:
// vite.config.ts
import { defineConfig } from "vite"
import { RegexWandTransformPlugin } from "regex-wand/transform"
export default defineConfig({
plugins: [RegexWandTransformPlugin.vite()],
})The transform recognizes static defineRegex, createRegExp,
createExactRegExp, createRegExpWithFlags, and
createExactRegExpWithFlags calls imported from regex-wand. Dynamic
expressions are left unchanged. The emitted code preserves the adapter shape, so
.magic, .ark, and .toRegExp() keep working.
For expressions that are valid at runtime but too complex or dynamic for
ArkRegex to infer, use fromMagicAs to provide the result type manually:
import { createRegExp, digit } from "magic-regexp"
import { fromMagicAs } from "regex-wand"
const magicRoute = createRegExp("/users/", digit.times.atLeast(1).as("userId"))
const route = fromMagicAs<
`${string}/users/${number}${string}`,
{ names: { userId: `${number}` } }
>(magicRoute)API
defineRegex({ inputs, match, flags })is the recommended object-shaped API.inputsis a Magic Regex-compatible tuple,matchis"contains"by default or"exact"for start/end anchoring, andflagsaccepts helper values, arrays, strings, or Sets.patternis also accepted as an alias for teams that prefer domain wording.createRegExp(...inputs)is the Magic Regex-compatible positional builder for contains-style regexes.createExactRegExp(...inputs)is a positional helper for start/end anchored regexes.createRegExpWithFlags(inputs, ...flags)is a positional helper for Magic Regex flag helpers.createRegExpWithFlags(inputs, flags)also accepts flag arrays, flag strings, and flag Sets.createExactRegExpWithFlags(inputs, ...flags)combines anchoring and flags.fromMagic(magic)adapts an existingMagicRegExp.fromMagicAs<Pattern, Context>(magic)adapts an existingMagicRegExpwith manually supplied ArkRegex result types.RegexWandTransformPluginfromregex-wand/transformcompiles staticregex-wandbuilder calls at build time.WandCompatibilityErrormarks strict type-level conversion failures.
All Magic Regex primitives are re-exported from the package.
Runtime Contract
The returned value is a native RegExp with extra typed properties attached:
magicis the original Magic RegexRegExp.arkis an alias to the adapted value for explicit interop.toRegExp()returns a new plainRegExpwith the same source and flags.
Plain string inputs are escaped by Magic Regex. Magic Regex fragments keep their
composition behavior. Native RegExp rules still apply at runtime, including
duplicate flag errors and lastIndex behavior for global or sticky regexes.
Type Safety Contract
regex-wand is strict by default. If the adapter cannot preserve the ArkRegex
type benefit for a Magic Regex value, the return type carries a compatibility
error instead of silently degrading to a plain RegExp.
See docs/type-safety.md for the longer version. See docs/arktype-interop.md for when to use ArkType schema regexes directly. See docs/support.md for the explicit support matrix and known technical gaps. See docs/roadmap.md for the upstream compatibility audit and future support plan.
Verification
The package test suite covers both runtime behavior and compile-time inference:
- Vitest runtime tests for builders, exact/contains matching, escaped strings,
flag helpers/arrays/strings/Sets, indices, named groups, optional captures,
lookarounds, backreferences, manual typing, string
RegExpprotocols, andlastIndex. - Vitest transform tests for direct builder calls, namespaced builder calls, and dynamic expressions that must be left untouched.
tsdtests for inferred strings, captures, named groups, flags, narrowing, manual typing,RegexParts, and compatibility errors.- A runtime import guard to ensure built browser code does not import ArkRegex.
- A packed-consumer test that installs the generated tarball into a temporary
project and verifies TypeScript, runtime behavior, and the
regex-wand/transformVite subpath. - TanStack Intent skill validation.
See docs/testing.md for the test matrix and what each layer proves.
Run everything from the monorepo root:
bun run release:checkAgent Skill
This package ships a TanStack Intent skill under skills/core/SKILL.md so coding
agents can load version-aligned usage guidance from the installed npm package.
Development And Publishing
bun install
bun run check
bun run test:coverageUseful monorepo commands:
bun run release:check
bun run publish:dry-run
bun run publish:regex-wand
bun run registry:checkAutomated publishing lives in the monorepo release workflow:
.github/workflows/release.yml.
The workflow publishes public GitHub releases to npm with provenance through npm trusted publishing. Local publishing remains a fallback for emergencies.
publish:regex-wand runs bun publish --access public in the package workspace
and should only be used as a local fallback. Bump packages/regex-wand/package.json
before publishing because npm does not allow republishing the same version.
