The RegExp v Flag: Intersection, Subtraction and Properties of Strings
The v flag rewrites JavaScript's character-class grammar: intersection with &&, subtraction with --, nested classes and properties of strings. What shipped, and what breaks on upgrade.
JavaScript has had regular expressions since ECMAScript 3 in 1999, and for nearly all of that run a character class could do exactly one thing with sets: union.[6] [A-Z0-9] means the union of [A-Z] and [0-9], and there was no syntax for anything else.[2] If you needed the intersection of two Unicode properties, or a property with a single exception carved out of it, you hardcoded the resulting ranges or reached for a lookahead assertion that no reader would recognise as a set operation.[4]
The v flag changes the grammar itself. It adds intersection, subtraction, nested character classes, and multi-code-point string literals.[4]
v Is a Replacement for u, Not an Addition
The first thing to internalise is that v and u are mutually exclusive modes, not layers. RegExp.prototype.unicodeSets reports whether the v flag was used on a given regex.[1] In the specification it is defined as an accessor property whose set accessor is undefined, with the getter testing the code unit 0x0076.[9] The v flag is an upgrade to u that enables more Unicode-related features, and because the two interpret the same pattern in incompatible ways, supplying both flags is a SyntaxError.[1]
That incompatibility is deliberate. The proposal's authors made backwards compatibility an explicit goal: no pattern that currently parses should change meaning.[4] Because set notation needs new syntax inside character classes, and some of that syntax was previously valid as literal characters, the new behaviour had to sit behind a new flag. An in-expression modifier was considered and rejected because ECMAScript does not use them yet, and a prefix form was rejected as developer-hostile. The letter after u was the clean answer.[4]
The practical consequence: v implies everything u does, plus the new grammar. You cannot opt into set notation while keeping u's parsing of character classes.[1]
Subtraction: The Exception You Could Never Express
Subtraction is written A--B and matches strings in A but not in B.[2] The operands of -- can be a character, a character escape, a character class escape, or even another character class.[2]
The canonical example is finding non-ASCII decimal digits, the ones you would want to normalise before comparing:
const nonAsciiDigits = /[\p{Decimal_Number}--[0-9]]/v;
nonAsciiDigits.test("𑜹"); // true — a non-ASCII digit
nonAsciiDigits.test("4"); // falseSubtracting a single character needs no nesting at all: [\p{Script_Extensions=Greek}--π] matches Greek symbols except pi.[6] Before v, expressing "decimal digits except ASCII" meant enumerating Unicode ranges by hand and re-deriving them on every Unicode release.[4]
Intersection: Both This and That
A&&B matches strings that are in both A and B.[2] The example that shows why this matters is Greek letters, because the Greek script includes numerals and signs alongside letters:
const greekLetters = /[\p{Script_Extensions=Greek}&&\p{Letter}]/v;
greekLetters.test("π"); // true — U+03C0 GREEK SMALL LETTER PI
greekLetters.test("𐆊"); // false — U+1018A GREEK ZERO SIGNThe same shape works for ASCII whitespace: [\p{White_Space}&&\p{ASCII}] matches a newline but not U+2028 LINE SEPARATOR.[6] Both patterns express a rule that had no direct spelling before the flag existed, and the engine now does the set arithmetic instead of you keeping a hand-computed range table in sync with the Unicode database.[6]
Nested Classes and the No-Mixing Rule
Operators cannot be chained at the same level. [\w&&[A-z]--_] is a syntax error; you nest to disambiguate, writing [\w&&[[A-z]--_]] or [[\w&&[A-z]]--_], which both mean [A-Za-z].[2] The same restriction bites in plain cases: [AB--C] is invalid, and you write [A[B--C]] instead.[2] Union remains the default when no operator is present.[2]
Properties of Strings: Matching Things That Are Not One Code Point
This part has nothing to do with set algebra and everything to do with correctness. \p{Emoji} in u mode matches a single code point, so it matches a single-code-point emoji but not one composed of several code points.[6]
The Unicode Standard also defines properties of strings, which expand to sets of strings rather than sets of code points.[6] In v mode, \p can match those, so a pattern anchored with \p{RGI_Emoji} matches the whole sequence regardless of how many code points it happens to contain.[6]
RGI_Emoji is defined in UTS #51 as the subset of all valid emoji recommended for general interchange.[8]
Seven properties of strings ship with the flag: Basic_Emoji, Emoji_Keycap_Sequence, RGI_Emoji_Modifier_Sequence, RGI_Emoji_Flag_Sequence, RGI_Emoji_Tag_Sequence, RGI_Emoji_ZWJ_Sequence, and RGI_Emoji.[6] Two caveats are worth internalising. First, \P cannot be used with them: complementing a property of strings is unclear, because "a string that does not have a property" gives no answer to how many characters to consume, so it throws.[3] Second, v mode does not interpret grapheme clusters as single characters — they remain multiple code points.[1]
\q{...}: String Literals Inside a Class
Multi-code-point strings can also be enumerated by hand with \q{...}. The pattern \q{a|bc|def} matches the strings a, bc, and def.[6]
Alternatives are attempted in descending order of length regardless of the order you write them, so the match is greedy: [\q{a|ab}], [\q{ab|a}], [\q{ab}a], and [a\q{ab}] all match ab given ab as input.[2]
Without \q{...} it would be impossible to subtract hardcoded multi-character strings.[6] Complement classes cannot contain them unless every element is a single character, so [^\q{ab|c}] is invalid.[2]
The Case-Insensitive Fix You Did Not Know You Needed
Under u with the i flag, two patterns that look like double negations of each other behave nothing alike:
const string = "aAbBcC4#";
string.replaceAll(/\p{Lowercase_Letter}/giu, "X"); // "XXXXXX4#"
string.replaceAll(/[^\P{Lowercase_Letter}]/giu, "X"); // "aAbBcC4#"The reason is ordering. In u mode, [^...] negates the match result after case folding, so the expected character set can end up outside the case-folded input space and the negation silently stops working. In v mode the complement is taken over the case-folded universe, so the operations cancel properly.[2] Under v, both lines above return the same string, and more generally [^\p{X}], [\P{X}] and \P{X} become equivalent whether or not i is set.[6]
What Breaks When You Upgrade
Because the class syntax got more sophisticated, more characters are reserved. In addition to ] and \, the characters (, ), [, {, }, /, -, and | must be escaped inside a class if they are meant literally.[2] Double punctuator sequences must be escaped as well — &&, !!, ##, $$, %%, **, ++, ,,, .., ::, ;;, <<, ==, >>, ??, @@, ^^, and ~~ — along with a doubled backtick character.[2] The escape rules also invert in places: [(] is valid under u and invalid under v, while [\!] is invalid under u as an identity escape and valid under v.[4]
A migration checklist: swap u for v, escape the newly reserved literals, and audit every [^...] that runs with the i flag, because that is where the semantics genuinely changed rather than merely tightened.
Shipping Status
Chrome and Edge 112, Firefox 116, Safari 17, Node.js 20.0.0, Deno 1.32, and Bun 1.0.0 all support the flag.[5] The feature set is tracked as the regexp group in web-features, whose compatibility list includes javascript.builtins.RegExp.unicodeSets.[7]
The proposal reached Stage 4 at the 2023-05-16 TC39 meeting and was merged into the ECMAScript specification on 2023-06-15, on track for the ES2024 snapshot.[4]
V8 shipped the functionality experimentally behind --harmony-regexp-unicode-sets in V8 v11.0, corresponding to Chrome 110, before enabling it by default in v12.0.[6] Babel has supported transpiling the flag since 7.17.0, and a change to the HTML Standard has been proposed to bring the same features to the pattern attribute.[6]
Sources
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets — MDN — RegExp.prototype.unicodeSets
[2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_class — MDN — Character class: [...], [^...] (v-mode)
[3] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape — MDN — Unicode character class escape \p{...}
[4] https://github.com/tc39/proposal-regexp-v-flag — TC39 — RegExp v flag with set notation + properties of strings (Stage 4)
[5] https://github.com/mdn/browser-compat-data/blob/main/javascript/builtins/RegExp.json — mdn/browser-compat-data — RegExp.json (unicodeSets)
[6] https://v8.dev/features/regexp-v-flag — V8 — RegExp v flag with set notation and properties of strings (2022)
[7] https://github.com/web-platform-dx/web-features/blob/main/features/regexp.yml — web-features — regexp.yml
[8] https://www.unicode.org/reports/tr51 — UTS #51 — Unicode Emoji
[9] https://tc39.es/ecma262/multipage/text-processing.html — ECMA-262 — RegExp.prototype.unicodeSets