JavaScript正規表現完全実践ガイド〜RegExp・lookbehind・名前付きグループ・Unicode・実用パターン50選【2026年版】〜

JavaScript の 正規表現(RegExp) は、フォーム検証・ログ解析・テンプレート展開・スクレイピング前処理・コード自動生成まで、フロントエンドからバックエンドまでありとあらゆる場面で顔を出します。にもかかわらず「読めない」「書けない」「壊れる」「ReDoS で止まる」と苦手意識を持つエンジニアが非常に多いのも事実です。原因はシンプルで、JavaScript の正規表現は ES2018 で名前付きグループと lookbehind が入り、ES2024 で v フラグ(集合演算)が標準化されるなど、進化が早すぎて古い知識のままアップデートが止まっているからです。

本記事では ECMAScript 2025 / TypeScript 5.x 準拠で、コピペで動く 40 以上のコードサンプルを通して、リテラル vs new RegExp() の使い分けから、test / exec / matchAll / replace / split の挙動差、g / i / m / s / u / y / d / v 全フラグの意味、名前付きグループ・lookahead/lookbehind・Unicode property・v フラグの集合演算、メール・URL・パスワード強度・クレジットカードといった現実的なパターン、そして ReDoS 対策・パフォーマンス・TypeScript Template Literal Types との連携まで、正規表現単体で完結する実践知見を完全網羅します。

本記事は 正規表現そのものに完全特化しているため、配列メソッド完全実践ガイド(配列処理の観点)、Map/Set 完全実践ガイド(コレクションの観点)、Template Literal Types 解説記事(TypeScript 型の観点)とは別軸のレイヤーを扱います。正規表現を素手で書けるエンジニアは、エディタの「検索置換」から CI の grep スクリプトまで、生産性が一段上がります。

  1. 1. 正規表現の作り方とリテラル vs new RegExp()
    1. 1.1 リテラル記法の基本
    2. 1.2 new RegExp() で動的に組み立てる
    3. 1.3 ユーザー入力は必ずエスケープする
    4. 1.4 リテラル vs コンストラクタの選択基準
  2. 2. test と exec の使い分け
    1. 2.1 test は真偽値だけ返す
    2. 2.2 exec はキャプチャ込みの結果を返す
    3. 2.3 g フラグ付き exec のステートフルな挙動
  3. 3. String 側のメソッド5種を使い分ける
    1. 3.1 match は g フラグ有無で結果型が変わる
    2. 3.2 matchAll は反復子でキャプチャも全部取れる
    3. 3.3 replace と replaceAll
    4. 3.4 replace のコールバック引数
    5. 3.5 search と split
  4. 4. フラグ完全リファレンス(g / i / m / s / u / y / d / v)
    1. 4.1 g(global)と i(ignore case)
    2. 4.2 m(multiline)で ^$ が行頭行末に
    3. 4.3 s(dotAll)で . が改行にもマッチ
    4. 4.4 u(Unicode)はもう必須フラグ
    5. 4.5 y(sticky)は lastIndex から「ピッタリ」マッチ
    6. 4.6 d(indices)でマッチ位置を取得
    7. 4.7 v(unicodeSets)は ES2024 の集合演算
  5. 5. メタ文字と文字クラスの基礎
    1. 5.1 ドット(.)・選択(|)・エスケープ()
    2. 5.2 文字クラス([] と [^])
    3. 5.3 ショートハンド(d w s と大文字版)
  6. 6. 量指定子と貪欲・非貪欲
    1. 6.1 *・+・? の基本
    2. 6.2 {n,m} で回数を指定する
    3. 6.3 貪欲(greedy)と非貪欲(lazy)
  7. 7. グループ・キャプチャ・後方参照
    1. 7.1 普通のキャプチャグループ
    2. 7.2 非キャプチャグループ(?:…)
    3. 7.3 後方参照(1, 2, …)
    4. 7.4 名前付きキャプチャ(?<name>…)
    5. 7.5 名前付きグループの後方参照と置換
  8. 8. lookahead と lookbehind
    1. 8.1 lookahead(?=…)と否定先読み(?!…)
    2. 8.2 lookbehind(?<=…)と否定後読み(?<!…)
    3. 8.3 lookaround の組み合わせでパスワード強度
  9. 9. アンカーと単語境界
    1. 9.1 ^ と $
    2. 9.2 b と B(単語境界)
  10. 10. Unicode property と日本語マッチ
    1. 10.1 p{Letter} p{Number} などの基本
    2. 10.2 Script プロパティ(ひらがな・カタカナ・漢字)
    3. 10.3 絵文字を 1 文字として扱う
  11. 11. 実用パターン:メール・URL・IP・電話番号
    1. 11.1 メールアドレスの現実的なパターン
    2. 11.2 URL 検証
    3. 11.3 IPv4 / IPv6 アドレス
    4. 11.4 日本の電話番号と郵便番号
  12. 12. 実用パターン:日付・クレジットカード・パスワード
    1. 12.1 日付フォーマット(YYYY-MM-DD)
    2. 12.2 ISO 8601 とタイムスタンプ
    3. 12.3 クレジットカード番号(形式 + Luhn)
    4. 12.4 パスワード強度の段階判定
  13. 13. テンプレート展開・カラーコード・スラッグ
    1. 13.1 テンプレート文字列展開
    2. 13.2 CSS カラーコードのパース
    3. 13.3 スラッグ生成(日本語対応)
  14. 14. ログ解析と CSV パース
    1. 14.1 Apache / Nginx アクセスログ
    2. 14.2 CSV の素朴なパース
    3. 14.3 マークダウンの簡易パース
  15. 15. ReDoS 対策とパフォーマンス
    1. 15.1 ReDoS の典型例
    2. 15.2 安全な書き換えパターン
    3. 15.3 タイムアウト付き実行
    4. 15.4 ベンチマークの取り方
  16. 16. TypeScript Template Literal Types との連携
    1. 16.1 型ガードとしての正規表現
    2. 16.2 ブランド型と組み合わせる
    3. 16.3 名前付きグループの型推論
  17. 17. デバッグツールとテスト戦略
    1. 17.1 regex101 と Debuggex
    2. 17.2 テストケース駆動で正規表現を書く
    3. 17.3 デバッグ用ヘルパー関数
  18. 18. よくあるハマりどころとアンチパターン
    1. 18.1 g フラグ + test の lastIndex 罠
    2. 18.2 HTML を正規表現でパースしようとしない
    3. 18.3 文字列リテラル内のバックスラッシュ
    4. 18.4 まとめ:正規表現を書く順序
  19. 19. まとめ:正規表現は型より先に身につける投資

1. 正規表現の作り方とリテラル vs new RegExp()

JavaScript で正規表現を作る方法は 2 つだけです。スラッシュで囲むリテラル記法new RegExp() コンストラクタです。動的に組み立てたいときだけコンストラクタを使い、それ以外はリテラルが基本です。

1.1 リテラル記法の基本

リテラル記法はパース時に 1 回だけコンパイルされるため、ループ内で再利用しても高速です。ほとんどのケースはこれで足ります。

// リテラル記法
const re = /hello/i;
console.log(re.test("Hello World")); // true
console.log(re.test("goodbye"));     // false

// フラグはスラッシュの後ろにまとめて書く
const re2 = /d+/g;
console.log("a1 b22 c333".match(re2)); // ["1", "22", "333"]

1.2 new RegExp() で動的に組み立てる

変数からパターンを組み立てたいときは new RegExp(pattern, flags) を使います。ただし バックスラッシュは文字列リテラルの中で 2 重になる点に注意してください。

// new RegExp で動的生成
const keyword = "hello";
const re = new RegExp(keyword, "i");
console.log(re.test("Hello World")); // true

// バックスラッシュは2重
const reDigit = new RegExp("\d+", "g");
console.log("a1 b22".match(reDigit)); // ["1", "22"]

1.3 ユーザー入力は必ずエスケープする

ユーザー入力をそのまま new RegExp() に渡すと、.* がメタ文字として解釈されて意図しないマッチや例外を引き起こします。必ずエスケープ関数を通してください。

// ユーザー入力をエスケープする関数
function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[]\]/g, "\$&");
}

const userInput = "file.name+v2.txt";
const safe = new RegExp(escapeRegExp(userInput));
console.log(safe.test("file.name+v2.txt")); // true
console.log(safe.test("fileXnameXv2Xtxt")); // false(. が文字通り扱われる)

1.4 リテラル vs コンストラクタの選択基準

判断基準はとてもシンプルです。パターンが固定ならリテラル、動的に組み立てるならコンストラクタ。それだけです。

// 推奨:固定パターンはリテラル
const EMAIL = /^[^s@]+@[^s@]+.[^s@]+$/;

// 推奨:動的はコンストラクタ
function buildSearchRegex(keywords) {
  const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[]\]/g, "\$&"));
  return new RegExp(escaped.join("|"), "gi");
}

const re = buildSearchRegex(["fetch", "Promise", "async/await"]);
console.log("Promise と async/await".match(re));
// ["Promise", "async/await"]

2. test と exec の使い分け

正規表現オブジェクト自身が持つメソッドは testexec の 2 つだけです。真偽値だけ欲しいときは test、キャプチャグループの結果まで取りたいときは execと覚えれば十分です。

2.1 test は真偽値だけ返す

test はマッチするかどうかだけを true/false で返します。分岐条件で使う最速の手段です。

// test:真偽値だけ
const isNumeric = /^d+$/;
console.log(isNumeric.test("12345")); // true
console.log(isNumeric.test("12a45")); // false

if (/^https?:///.test(url)) {
  console.log("HTTP/HTTPS な URL です");
}

2.2 exec はキャプチャ込みの結果を返す

exec はマッチした文字列と各キャプチャグループを配列で返します。マッチしない場合は null を返すので、分割代入する前に必ず null チェックしてください。

// exec:キャプチャ込み
const re = /^(d{4})-(d{2})-(d{2})$/;
const m = re.exec("2026-05-27");

if (m) {
  const [, year, month, day] = m;
  console.log(year, month, day); // 2026 05 27
}

// マッチしないと null
console.log(re.exec("not a date")); // null

2.3 g フラグ付き exec のステートフルな挙動

g フラグを付けた正規表現の execlastIndex という内部状態を持ち、呼び出すたびに次のマッチを返すイテレータのような挙動になります。古い書き方なので、現代では後述の matchAll をおすすめします。

// g フラグ + exec のループ(古典的)
const re = /d+/g;
const text = "a1 b22 c333";

let m;
while ((m = re.exec(text)) !== null) {
  console.log(`${m[0]} @ ${m.index} (lastIndex=${re.lastIndex})`);
}
// 1 @ 1 (lastIndex=2)
// 22 @ 4 (lastIndex=6)
// 333 @ 8 (lastIndex=11)

3. String 側のメソッド5種を使い分ける

文字列側にも正規表現を受け取るメソッドが揃っています。match / matchAll / replace / replaceAll / search / split の 6 種類。g フラグの有無で挙動が大きく変わるのがハマりどころです。

3.1 match は g フラグ有無で結果型が変わる

g なしの matchexec 相当の結果(キャプチャ込み)、g 付きの matchマッチ文字列だけの配列を返します。挙動の差を意識しないと、キャプチャが取れない罠にハマります。

// g なし:キャプチャ込み1件
const m1 = "2026-05-27".match(/^(d{4})-(d{2})-(d{2})$/);
console.log(m1);
// ["2026-05-27", "2026", "05", "27", index: 0, ...]

// g 付き:マッチ文字列だけの配列
const m2 = "a1 b22 c333".match(/d+/g);
console.log(m2); // ["1", "22", "333"]

// g 付きでマッチしないと null(空配列ではない!)
console.log("xxx".match(/d+/g)); // null

3.2 matchAll は反復子でキャプチャも全部取れる

ES2020 で入った matchAll は、g フラグ必須で、全マッチをキャプチャ込みでイテレートできる現代的な API です。今から書くコードはこれ一択でほぼ問題ありません。

// matchAll:キャプチャ込みで全マッチ
const text = "name=alice; age=30; city=tokyo";
const re = /(w+)=(w+)/g;

for (const m of text.matchAll(re)) {
  console.log(`${m[1]} -> ${m[2]}`);
}
// name -> alice
// age -> 30
// city -> tokyo

// 配列化したいなら Array.from
const all = Array.from(text.matchAll(re), (m) => [m[1], m[2]]);
console.log(all); // [["name","alice"], ["age","30"], ["city","tokyo"]]

3.3 replace と replaceAll

replaceg フラグなしだと最初の 1 件だけ置換します。全件置換したいときは g フラグを付けるか、ES2021 の replaceAll を使います。replaceAll正規表現を渡すなら必ず g フラグ必須です。

// replace:g なしは最初の1件
console.log("a1 b2 c3".replace(/d/, "x")); // "ax b2 c3"

// replace + g:全件
console.log("a1 b2 c3".replace(/d/g, "x")); // "ax bx cx"

// replaceAll:文字列でも正規表現でも OK(正規表現は g 必須)
console.log("a1 b2 c3".replaceAll(/d/g, "x")); // "ax bx cx"

// replaceAll に g なし正規表現を渡すと TypeError
// "a1".replaceAll(/d/, "x"); // TypeError

3.4 replace のコールバック引数

replace の第 2 引数には 関数が渡せて、マッチごとに動的に置換文字列を返せます。引数は (match, ...groups, offset, string, namedGroups) の順です。

// コールバック:動的置換
const result = "price: 1000 yen, tax: 100".replace(
  /(d+)/g,
  (match, num) => Number(num).toLocaleString("en-US")
);
console.log(result); // "price: 1,000 yen, tax: 100"

// 名前付きグループも受け取れる
const result2 = "2026-05-27".replace(
  /(?d{4})-(?d{2})-(?d{2})/,
  (...args) => {
    const groups = args.at(-1); // 最後の引数が namedGroups
    return `${groups.d}/${groups.m}/${groups.y}`;
  }
);
console.log(result2); // "27/05/2026"

3.5 search と split

search はマッチ位置を返すだけ、split は正規表現で文字列を分割します。カンマや空白の任意連続を区切りにしたいときsplit が便利です。

// search:最初のマッチ位置(なければ -1)
console.log("hello world 123".search(/d+/)); // 12
console.log("hello world".search(/d+/));     // -1

// split:複数文字を区切り文字に
console.log("a, b ,  c;d".split(/[,;]s*/));
// ["a", "b ", "c", "d"]

// split + キャプチャは区切り文字も結果に含む
console.log("a1b2c".split(/(d)/));
// ["a", "1", "b", "2", "c"]

4. フラグ完全リファレンス(g / i / m / s / u / y / d / v)

JavaScript の正規表現には 8 つのフラグがあり、それぞれ独立して指定できます。大文字小文字を無視するなら i、複数行で ^$ を行頭行末にしたいなら m. を改行にもマッチさせたいなら s。意味を覚えてしまえば怖くありません。

4.1 g(global)と i(ignore case)

g は全マッチ、i は大文字小文字無視。もっとも頻繁に登場する 2 つです。

// g:全マッチ
console.log("Aa Bb Cc".match(/a/g));  // ["a"](大文字Aは拾わない)
console.log("Aa Bb Cc".match(/a/gi)); // ["A", "a"]

// i:大文字小文字を無視
console.log(/javascript/i.test("JavaScript")); // true
console.log(/javascript/.test("JavaScript"));  // false

4.2 m(multiline)で ^$ が行頭行末に

m フラグを付けると、^$ が文字列全体ではなく各行の先頭末尾にマッチするようになります。複数行ログの解析で多用します。

// m なし:^ は文字列全体の先頭
const log = "ERROR: foonWARN: barnERROR: baz";
console.log(log.match(/^ERROR.*/));  // ["ERROR: foo"]

// m あり:各行の先頭にマッチ
console.log(log.match(/^ERROR.*/gm));
// ["ERROR: foo", "ERROR: baz"]

4.3 s(dotAll)で . が改行にもマッチ

デフォルトでは . は改行(n)にマッチしません。s フラグを付けると 「改行を含むあらゆる 1 文字」になります。HTML タグ内の複数行抽出で必須です。

const html = "
line1nline2
"; // s なし:改行を含むと拾えない console.log(html.match(/
(.+)
/)); // null // s あり:改行込みで拾える console.log(html.match(/
(.+)
/s)); // ["
line1nline2
", "line1nline2"]

4.4 u(Unicode)はもう必須フラグ

u フラグを付けると、サロゲートペアを 1 文字として扱い、Unicode property エスケープ(p{...})が使えるようになります。日本語や絵文字を扱うなら必須です。

// u なし:絵文字が2文字として扱われる
console.log("👨‍💻".length);            // 5(サロゲート + ZWJ)
console.log(/^.$/.test("😀"));         // false
console.log(/^.$/u.test("😀"));        // true(u 付きなら1文字)

// Unicode property は u 必須
console.log(/p{Letter}/u.test("あ")); // true
console.log(/p{Letter}/.test("あ"));  // SyntaxError(u なしでは使えない)

4.5 y(sticky)は lastIndex から「ピッタリ」マッチ

y フラグは lastIndex の位置にピッタリマッチしないと失敗します。レキサー(字句解析)を書くときに重宝します。

// y:lastIndex の位置から「のみ」マッチ
const re = /d+/y;
re.lastIndex = 4;
console.log(re.exec("ab 1234 cd"));
// ["1234"]:位置4は "1" でマッチ成功

re.lastIndex = 3;
console.log(re.exec("ab 1234 cd"));
// null:位置3は " " で即失敗(g なら次まで探すが y は探さない)

4.6 d(indices)でマッチ位置を取得

ES2022 で入った d フラグを付けると、結果オブジェクトに indices プロパティが付き、各キャプチャの開始終了位置が取れます。エディタのシンタックスハイライト実装などで便利です。

// d:indices プロパティが付く
const re = /(w+) (w+)/d;
const m = re.exec("hello world");

console.log(m.indices);
// [[0, 11], [0, 5], [6, 11]]
//  ^全体    ^group1 ^group2

// 名前付きグループの位置も取れる
const re2 = /(?d{4})-(?d{2})/d;
const m2 = re2.exec("2026-05");
console.log(m2.indices.groups);
// { year: [0, 4], month: [5, 7] }

4.7 v(unicodeSets)は ES2024 の集合演算

ES2024 で入った v フラグは u の上位互換で、文字クラスの中で &&(積)--(差)||(和)が使えます。Unicode property を組み合わせた高度なマッチを書きたいときに強力です。

// v:集合演算
// 「文字」かつ「ASCII ではない」= 非 ASCII 文字
const reNonAscii = /[p{Letter}--p{ASCII}]/v;
console.log(reNonAscii.test("a")); // false
console.log(reNonAscii.test("あ")); // true

// 「数字」または「ひらがな」
const reMix = /[p{Number}p{Script=Hiragana}]/v;
console.log(reMix.test("3")); // true
console.log(reMix.test("あ")); // true
console.log(reMix.test("A")); // false

// 「ひらがな」かつ「小書き文字を除く」
const reHira = /[p{Script=Hiragana}--[ぁぃぅぇぉっゃゅょ]]/v;
console.log(reHira.test("あ")); // true
console.log(reHira.test("ぁ")); // false

5. メタ文字と文字クラスの基礎

正規表現の強さの源泉はメタ文字です。. ^ $ * + ? | ( ) [ ] { } の意味を体に染み込ませると、ほとんどの場面で何を書けばいいかが瞬時にわかります。

5.1 ドット(.)・選択(|)・エスケープ()

. は改行以外の任意の 1 文字、| は OR、 はエスケープ。基本中の基本です。

// . は任意の1文字(s なしなら改行除く)
console.log(/h.llo/.test("hello")); // true
console.log(/h.llo/.test("hxllo")); // true

// | は選択(OR)
console.log(/cat|dog|bird/.test("I have a dog")); // true

//  でメタ文字を文字通り扱う
console.log(/3.14/.test("π=3.14"));  // true
console.log(/3.14/.test("3X14"));    // false

5.2 文字クラス([] と [^])

角括弧の中に書いた文字のいずれか 1 文字にマッチします。先頭に ^ を付けると否定(その文字以外)になります。範囲指定(a-z)も使えます。

// 文字クラス
console.log(/[aeiou]/.test("hello"));   // true
console.log(/[A-Z]/.test("hello"));     // false
console.log(/[A-Za-z0-9]/.test("_"));   // false

// 否定文字クラス
console.log(/[^0-9]/.test("12345"));    // false
console.log(/[^0-9]/.test("12a45"));    // true

// 範囲指定の組み合わせ
const slug = /^[a-z0-9-]+$/;
console.log(slug.test("hello-world-1")); // true
console.log(slug.test("Hello"));         // false

5.3 ショートハンド(d w s と大文字版)

頻出パターンには短縮記法があります。d は数字、w は単語文字、s は空白。大文字版はそれぞれの否定です。

// ショートハンド
// d = [0-9]、D = [^0-9]
// w = [A-Za-z0-9_]、W = [^A-Za-z0-9_]
// s = 空白(スペース、タブ、改行など)、S = それ以外

console.log("price: 1000 yen".match(/d+/g));   // ["1000"]
console.log("hello_world".match(/w+/g));       // ["hello_world"]
console.log("a btcnd".split(/s+/));          // ["a", "b", "c", "d"]

// 注意:w は ASCII のみ。日本語は拾わない
console.log("こんにちは".match(/w+/g)); // null
// 日本語を含めたいなら p{L} + u フラグ
console.log("こんにちは".match(/p{L}+/gu)); // ["こんにちは"]

6. 量指定子と貪欲・非貪欲

「何回繰り返すか」を指定するのが量指定子です。* + ? {n,m} の 4 種類だけ覚えれば、マッチの長さを思い通りにコントロールできます。

6.1 *・+・? の基本

* は 0 回以上、+ は 1 回以上、? は 0 回または 1 回。直前の要素に対して効きます。

// * = 0回以上
console.log(/ab*c/.test("ac"));    // true(b が0回)
console.log(/ab*c/.test("abbbc")); // true

// + = 1回以上
console.log(/ab+c/.test("ac"));    // false
console.log(/ab+c/.test("abc"));   // true

// ? = 0回 or 1回(任意)
console.log(/colou?r/.test("color"));  // true
console.log(/colou?r/.test("colour")); // true

6.2 {n,m} で回数を指定する

厳密な回数は {n}(ちょうど n 回)、{n,}(n 回以上)、{n,m}(n〜m 回)。パスワードの最小長検証などで多用します。

// {n} {n,} {n,m}
console.log(/d{4}/.test("2026"));       // true
console.log(/d{4,}/.test("12345"));     // true(4回以上)
console.log(/d{2,4}/.exec("123456789")); // ["1234"](最大4回まで貪欲に)

// 郵便番号(日本)
const zip = /^d{3}-d{4}$/;
console.log(zip.test("100-0001")); // true
console.log(zip.test("1000001"));  // false

6.3 貪欲(greedy)と非貪欲(lazy)

デフォルトの量指定子はできるだけ長くマッチしようとする「貪欲」です。後ろに ? を付けるとできるだけ短くマッチする「非貪欲(lazy)」になります。HTML タグ抽出ではほぼ必須の知識です。

const html = "foo and bar";

// 貪欲:できるだけ長く
console.log(html.match(/.*/));
// ["foo and bar"](最初のから最後のまで)

// 非貪欲:できるだけ短く
console.log(html.match(/.*?/g));
// ["foo", "bar"]

7. グループ・キャプチャ・後方参照

丸括弧 ()「グループ化」と「キャプチャ」の 2 つの役割を兼ねます。グループ化だけしてキャプチャしたくないときは (?:...) という非キャプチャグループを使います。

7.1 普通のキャプチャグループ

丸括弧で囲んだ部分は左から番号(1, 2, 3, …)が振られて、結果配列の対応するインデックスから取り出せます。

// キャプチャグループ
const m = "2026-05-27 12:34".match(/(d{4})-(d{2})-(d{2}) (d{2}):(d{2})/);
console.log(m[1], m[2], m[3], m[4], m[5]);
// 2026 05 27 12 34

// 0番目はマッチ全体
console.log(m[0]); // "2026-05-27 12:34"

7.2 非キャプチャグループ(?:…)

グループ化したいけれどキャプチャ番号は振りたくないとき、(?:...) を使います。番号がズレないので可読性が上がり、わずかにパフォーマンスも改善します。

// 非キャプチャグループ
const m1 = "http://example.com".match(/(?:https?)://(.+)/);
console.log(m1[1]); // "example.com"(プロトコルはキャプチャされない)

// 普通のグループだと番号がズレる
const m2 = "http://example.com".match(/(https?)://(.+)/);
console.log(m2[1], m2[2]); // "http" "example.com"

7.3 後方参照(1, 2, …)

マッチ中に同じグループでキャプチャした文字列を再度参照するのが後方参照です。HTML タグの対応や、重複単語の検出に使えます。

// 後方参照:同じタグで閉じる
console.log(/.*/.test("foo"));  // true
console.log(/.*/.test("foo"));  // false

// 重複単語の検出
const text = "this is is a test test";
console.log(text.match(/b(w+)s+1b/g));
// ["is is", "test test"]

7.4 名前付きキャプチャ(?<name>…)

ES2018 で入った名前付きグループは キャプチャに名前を付けて groups オブジェクト経由で取り出せる機能です。番号で覚えるより圧倒的に保守性が高くなります。

// 名前付きグループ
const re = /(?d{4})-(?d{2})-(?d{2})/;
const m = re.exec("2026-05-27");

console.log(m.groups.year);  // "2026"
console.log(m.groups.month); // "05"
console.log(m.groups.day);   // "27"

// 分割代入も気持ちいい
const { year, month, day } = m.groups;
console.log(`${year}/${month}/${day}`); // 2026/05/27

7.5 名前付きグループの後方参照と置換

名前付きグループも 後方参照(k<name>)と置換テンプレ($<name>)が使えます。replace で日付フォーマットを変えるのが典型例です。

// 名前付きグループの置換
const swapped = "2026-05-27".replace(
  /(?d{4})-(?d{2})-(?d{2})/,
  "$/$/$"
);
console.log(swapped); // "27/05/2026"

// 名前付き後方参照
const reTag = /<(?w+)>.*?</k>/;
console.log(reTag.test("foo")); // true
console.log(reTag.test("foo")); // false

8. lookahead と lookbehind

「マッチには含めないけれど、その前後に何があるかを条件にしたい」ときに使うのが先読み(lookahead)と後読み(lookbehind)です。パスワード強度検証で使う (?=...) や、価格抽出で使う (?<=¥) がそれです。

8.1 lookahead(?=…)と否定先読み(?!…)

(?=...)直後に〜が続く位置(?!...)直後に〜が続かない位置にマッチします。マッチ自体は消費しません。

// 先読み:直後に "yen" が続く数字を抽出
console.log("price: 1000 yen, tax: 100 yen".match(/d+(?= yen)/g));
// ["1000", "100"](" yen" は消費されない)

// 否定先読み:直後に "px" が来ない数字
console.log("100px 200 300em 400px".match(/d+(?!px|em)/g));
// 注:1文字ずつ評価されるので意図と違うことがある
//  → 単語境界を併用するのがコツ
console.log("100px 200 300em 400px".match(/bd+b(?!px|em)/g));
// ["200"]

8.2 lookbehind(?<=…)と否定後読み(?<!…)

(?<=...)直前に〜がある位置(?<!...)直前に〜がない位置。価格記号や接頭辞の除外に便利です。

// 後読み:¥ の後ろの金額だけ抽出
console.log("¥1000 と $20 と ¥3000".match(/(?<=¥)d+/g));
// ["1000", "3000"]

// 否定後読み:USD ではない金額
console.log("¥1000 USD2000 ¥3000".match(/(?<!USD)d+/g));
// ["1000", "000", "3000"]:単純すぎる
// → 単語境界を併用
console.log("¥1000 USD2000 ¥3000".match(/(?<![A-Z])bd+/g));
// ["1000", "3000"]

8.3 lookaround の組み合わせでパスワード強度

「英大文字を含む」「数字を含む」「記号を含む」を 複数の lookahead で AND 条件にして強度判定できます。これが lookahead 最大の実用例です。

// パスワード強度:8文字以上、大小英字・数字・記号を各1個以上
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[!@#$%^&*])[S]{8,}$/;

console.log(strong.test("Passw0rd!"));  // true
console.log(strong.test("password1"));  // false(大文字なし)
console.log(strong.test("PASSWORD1!")); // false(小文字なし)
console.log(strong.test("Pass!"));      // false(8文字未満)

9. アンカーと単語境界

アンカーは「位置」にマッチする特殊な要素です。^ は文字列の先頭、$ は末尾、b は単語境界。マッチの「範囲」を絞り込むのに必須です。

9.1 ^ と $

^ は先頭、$ は末尾に対応します。m フラグの有無で意味が変わるのは前述のとおりです。

// 完全一致(部分マッチを許さない)
console.log(/^d{3}-d{4}$/.test("100-0001"));    // true
console.log(/^d{3}-d{4}$/.test("〒100-0001"));  // false
console.log(/^d{3}-d{4}$/.test("100-0001 ext")); // false

9.2 b と B(単語境界)

b単語文字と非単語文字の境目(または文字列の端)にマッチします。「単語単位で置換したい」ときに必須です。

// b:単語境界
console.log("class classroom subclass".match(/bclassb/g));
// ["class"](classroom や subclass の中の class は拾わない)

// b なしだと全部拾う
console.log("class classroom subclass".match(/class/g));
// ["class", "class", "class"]

// 単語の置換
console.log("cat in catalog".replaceAll(/bcatb/g, "dog"));
// "dog in catalog"

10. Unicode property と日本語マッチ

日本語・絵文字・他言語を扱うなら Unicode property エスケープ(p{...})が必須です。u または v フラグと組み合わせて使います。

10.1 p{Letter} p{Number} などの基本

p{Letter} はあらゆる言語の文字、p{Number} はあらゆる数字記号にマッチします。wd は ASCII 限定なので、日本語を扱うなら p{...} を使うべきです。

// Unicode property
console.log("Hello こんにちは ¡Hola!".match(/p{Letter}+/gu));
// ["Hello", "こんにちは", "Hola"]

// d は ASCII のみ、p{Number} は多言語対応
console.log("価格は1,000円、税込み¥1100".match(/d+/g));        // ["1", "000", "1100"]
console.log("価格は1,000円、税込み¥1100".match(/p{Number}+/gu)); // 同じ(数字は ASCII で書かれている)

// 全角数字も拾う
console.log("価格1000円、税込1100円".match(/p{Number}+/gu));
// ["1000", "1100"]

10.2 Script プロパティ(ひらがな・カタカナ・漢字)

p{Script=Hiragana} のように 「文字体系(Script)」を指定すると、ひらがな・カタカナ・漢字を個別にマッチできます。日本語処理では最強の武器です。

// 文字体系で抽出
const text = "今日は2026年5月、桜のような暖かい日でした。";

console.log(text.match(/p{Script=Hiragana}+/gu));
// ["は", "の", "ような", "かい", "でした"]

console.log(text.match(/p{Script=Katakana}+/gu));
// null(この例にはカタカナなし)

console.log(text.match(/p{Script=Han}+/gu));
// ["今日", "年", "月", "桜", "暖", "日"]

// ひらがな+カタカナ+漢字(=日本語っぽい部分)
console.log(text.match(/[p{Script=Hiragana}p{Script=Katakana}p{Script=Han}]+/gu));
// ["今日は", "年", "月", "桜のような暖かい日でした"]

10.3 絵文字を 1 文字として扱う

絵文字は サロゲートペア + ZWJ シーケンスになっているため、単純な . では正しく拾えません。p{Emoji}p{Emoji_Presentation} が役立ちます。

// 絵文字を抽出
const text = "今日は☀ 明日は🌧 そのあと🚀✨";
console.log(text.match(/p{Emoji}/gu));
// ["☀", "🌧", "🚀", "✨"]

// 絵文字を全部取り除く
const cleaned = text.replace(/p{Emoji}/gu, "");
console.log(cleaned); // "今日は 明日は そのあと"

11. 実用パターン:メール・URL・IP・電話番号

ここからは現場でよく書く検証パターンを解説します。RFC 完全準拠の正規表現は非現実的に長くなるため、「実用的に十分な精度」を狙うのが原則です。

11.1 メールアドレスの現実的なパターン

RFC 5322 完全準拠の正規表現は数百文字になり保守不能です。「@ を 1 個含み、両側に文字がある」程度の素朴なチェック+ DNS / 送信検証に任せるのが現代の定石です。

// 実用的なメール検証(緩め)
const EMAIL = /^[^s@]+@[^s@]+.[^s@]+$/;

console.log(EMAIL.test("foo@example.com"));      // true
console.log(EMAIL.test("foo.bar+tag@sub.example.co.jp")); // true
console.log(EMAIL.test("not an email"));         // false
console.log(EMAIL.test("foo@bar"));              // false(ドットなし)

// 厳密にしたいなら HTML5 input[type=email] と同じパターンを参考に
const EMAIL_STRICT = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
console.log(EMAIL_STRICT.test("foo+tag@example.co.jp")); // true

11.2 URL 検証

URL も RFC 完全準拠は地獄です。URL コンストラクタの try/catch で検証する方が断然安全ですが、軽い形式チェックなら正規表現でも十分です。

// 軽い URL 検証
const URL_RE = /^https?://[^s/$.?#].[^s]*$/i;
console.log(URL_RE.test("https://example.com")); // true
console.log(URL_RE.test("ftp://example.com"));   // false

// 推奨:URL コンストラクタで確実に検証
function isValidUrl(s) {
  try {
    const u = new URL(s);
    return u.protocol === "http:" || u.protocol === "https:";
  } catch {
    return false;
  }
}
console.log(isValidUrl("https://example.com")); // true
console.log(isValidUrl("not a url"));           // false

11.3 IPv4 / IPv6 アドレス

IPv4 は各オクテットが 0〜255であることをチェックします。IPv6 は省略形を考えると複雑なので、軽い形式チェック+ node:netisIP 委譲が現実的です。

// IPv4(各オクテット0-255)
const IPV4 = /^(?:(?:25[0-5]|2[0-4]d|1d{2}|[1-9]?d).){3}(?:25[0-5]|2[0-4]d|1d{2}|[1-9]?d)$/;
console.log(IPV4.test("192.168.0.1"));   // true
console.log(IPV4.test("256.0.0.1"));     // false
console.log(IPV4.test("1.2.3"));         // false

// IPv6(軽い形式チェック)
const IPV6_LOOSE = /^[0-9a-fA-F:]+$/;
console.log(IPV6_LOOSE.test("::1"));                          // true
console.log(IPV6_LOOSE.test("2001:db8::8a2e:370:7334"));      // true

// 厳密判定は標準モジュール推奨(Node)
// import { isIP } from "node:net";
// console.log(isIP("2001:db8::8a2e:370:7334")); // 6

11.4 日本の電話番号と郵便番号

日本の電話番号は 市外局番のパターンが多種多様なので、現実的には桁数+ハイフン位置だけチェックすることが多いです。郵便番号は NNN-NNNN の形式が公式です。

// 日本の電話番号(緩め)
const TEL_JP = /^0d{1,4}-d{1,4}-d{3,4}$/;
console.log(TEL_JP.test("03-1234-5678"));    // true
console.log(TEL_JP.test("090-1234-5678"));   // true
console.log(TEL_JP.test("0120-123-456"));    // true

// 携帯のみ
const MOBILE_JP = /^(?:090|080|070)-d{4}-d{4}$/;
console.log(MOBILE_JP.test("090-1234-5678")); // true
console.log(MOBILE_JP.test("03-1234-5678"));  // false

// 郵便番号(日本)
const ZIP_JP = /^d{3}-d{4}$/;
console.log(ZIP_JP.test("100-0001"));    // true
console.log(ZIP_JP.test("〒100-0001"));  // false

// 〒記号を許容
const ZIP_JP_LOOSE = /^〒?d{3}-?d{4}$/;
console.log(ZIP_JP_LOOSE.test("〒1000001"));  // true
console.log(ZIP_JP_LOOSE.test("100-0001"));   // true

12. 実用パターン:日付・クレジットカード・パスワード

続いてフォーム検証で頻出の日付・カード番号・パスワードのパターンを実装します。型情報や Luhn チェックと組み合わせることで実用度がグッと上がります。

12.1 日付フォーマット(YYYY-MM-DD)

「形式が正しいか」と「実在する日付か」は別問題です。正規表現で形式だけチェックし、Date で実在検証するのが組み合わせとしてベストです。

// 形式チェック
const DATE_RE = /^(?d{4})-(?0[1-9]|1[0-2])-(?0[1-9]|[12]d|3[01])$/;

function isValidDate(s) {
  const m = DATE_RE.exec(s);
  if (!m) return false;
  const { y, m: mo, d } = m.groups;
  const date = new Date(`${y}-${mo}-${d}T00:00:00Z`);
  // 月・日が正規化で変わってないか確認(2月30日対策)
  return (
    date.getUTCFullYear() === Number(y) &&
    date.getUTCMonth() + 1 === Number(mo) &&
    date.getUTCDate() === Number(d)
  );
}

console.log(isValidDate("2026-05-27")); // true
console.log(isValidDate("2026-02-30")); // false(2月30日は存在しない)
console.log(isValidDate("2026-13-01")); // false(形式NG)

12.2 ISO 8601 とタイムスタンプ

API が返す日時は ISO 8601 がほぼ標準です。タイムゾーン部分(Z+09:00)を任意で許容するパターンが現場で頻出します。

// ISO 8601
const ISO = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}(?:.d{1,3})?(?:Z|[+-]d{2}:d{2})$/;

console.log(ISO.test("2026-05-27T12:34:56Z"));        // true
console.log(ISO.test("2026-05-27T12:34:56.789Z"));    // true
console.log(ISO.test("2026-05-27T12:34:56+09:00"));   // true
console.log(ISO.test("2026-05-27 12:34:56"));         // false

12.3 クレジットカード番号(形式 + Luhn)

クレジットカード番号は 「ブランドごとの形式」と「Luhn チェックサム」の 2 段階で検証します。正規表現はあくまでブランド判定用で、最終的な妥当性は Luhn が決めます。

// ブランド判定の正規表現
const CARD_PATTERNS = {
  visa:       /^4d{12}(?:d{3})?$/,
  mastercard: /^(?:5[1-5]d{14}|2(?:2[2-9]d|[3-6]d{2}|7[01]d|720)d{12})$/,
  amex:       /^3[47]d{13}$/,
  jcb:        /^(?:35d{14}|2131|1800d{11})$/,
};

function detectBrand(num) {
  const n = num.replace(/[s-]/g, "");
  for (const [brand, re] of Object.entries(CARD_PATTERNS)) {
    if (re.test(n)) return brand;
  }
  return null;
}

console.log(detectBrand("4111-1111-1111-1111")); // visa
console.log(detectBrand("3782 822463 10005"));   // amex

// Luhn チェックサム
function luhn(num) {
  const digits = num.replace(/D/g, "").split("").map(Number).reverse();
  const sum = digits.reduce((acc, d, i) => {
    if (i % 2 === 1) {
      const doubled = d * 2;
      return acc + (doubled > 9 ? doubled - 9 : doubled);
    }
    return acc + d;
  }, 0);
  return sum % 10 === 0;
}

console.log(luhn("4111 1111 1111 1111")); // true
console.log(luhn("4111 1111 1111 1112")); // false

12.4 パスワード強度の段階判定

真偽値だけでなく 「弱・中・強」の段階を返すのが UX 的にベストです。複数の lookahead を組み合わせて、満たした条件数で強度を決めます。

// 段階判定
function passwordStrength(pw) {
  if (pw.length < 8) return "weak";

  const checks = [
    /[a-z]/.test(pw),          // 小文字
    /[A-Z]/.test(pw),          // 大文字
    /d/.test(pw),             // 数字
    /[!@#$%^&*(),.?":{}|]/.test(pw), // 記号
    pw.length >= 12,            // 長さ12以上
  ];

  const score = checks.filter(Boolean).length;
  if (score >= 4) return "strong";
  if (score >= 3) return "medium";
  return "weak";
}

console.log(passwordStrength("password"));       // weak
console.log(passwordStrength("Password1"));      // medium
console.log(passwordStrength("Password1!Long")); // strong

13. テンプレート展開・カラーコード・スラッグ

正規表現は「データ抽出」だけでなく「テキスト生成」にも強力です。テンプレート文字列の {{var}} 展開や、スラッグ生成、CSS カラーコード解析を実装してみましょう。

13.1 テンプレート文字列展開

Mustache 風の {{name}} 形式を実装するのは正規表現 1 行で済みます。replace のコールバックで動的展開するのが定石です。

// {{var}} 展開
function render(template, vars) {
  return template.replace(/{{s*(w+)s*}}/g, (_, key) => {
    return vars[key] ?? "";
  });
}

const result = render("Hello {{name}}, you are {{age}} years old.", {
  name: "Alice",
  age: 30,
});
console.log(result); // "Hello Alice, you are 30 years old."

// ネストしたキーにも対応
function renderNested(template, vars) {
  return template.replace(/{{s*([w.]+)s*}}/g, (_, path) => {
    return path.split(".").reduce((o, k) => o?.[k], vars) ?? "";
  });
}
console.log(renderNested("Hello {{user.name}}", { user: { name: "Bob" } }));
// "Hello Bob"

13.2 CSS カラーコードのパース

HEX カラーは #RGB / #RRGGBB / #RRGGBBAA の 3 種類。名前付きグループで各成分を取り出し、数値化するのが現代的な書き方です。

// HEX カラーパース
function parseHex(s) {
  const m = /^#(?[da-f]{2})(?[da-f]{2})(?[da-f]{2})(?[da-f]{2})?$/i.exec(s);
  if (!m) return null;
  const { r, g, b, a } = m.groups;
  return {
    r: parseInt(r, 16),
    g: parseInt(g, 16),
    b: parseInt(b, 16),
    a: a ? parseInt(a, 16) / 255 : 1,
  };
}

console.log(parseHex("#ff8800"));    // {r:255,g:136,b:0,a:1}
console.log(parseHex("#ff880080"));  // {r:255,g:136,b:0,a:0.502}
console.log(parseHex("#xyz"));       // null

// rgb()/rgba() パース
function parseRgb(s) {
  const m = /^rgba?(s*(d+)s*,s*(d+)s*,s*(d+)s*(?:,s*([d.]+))?s*)$/.exec(s);
  if (!m) return null;
  return { r: +m[1], g: +m[2], b: +m[3], a: m[4] ? +m[4] : 1 };
}
console.log(parseRgb("rgba(255, 136, 0, 0.5)")); // {r:255,g:136,b:0,a:0.5}

13.3 スラッグ生成(日本語対応)

記事タイトルから URL 用のスラッグを自動生成するロジックは正規表現の独壇場です。日本語を許容するか、ASCII に絞るかでパターンが変わります。

// ASCII スラッグ
function toSlug(s) {
  return s
    .toLowerCase()
    .normalize("NFKD")                  // アクセント分解
    .replace(/[̀-ͯ]/g, "")    // アクセント記号削除
    .replace(/[^a-z0-9]+/g, "-")        // 英数字以外をハイフン
    .replace(/^-+|-+$/g, "");           // 端のハイフン削除
}

console.log(toSlug("Hello World! 2026"));            // "hello-world-2026"
console.log(toSlug("Café — résumé"));                // "cafe-resume"
console.log(toSlug("JavaScript 正規表現完全ガイド"));  // "javascript"(日本語は落ちる)

// 日本語残し版
function toSlugJP(s) {
  return s
    .toLowerCase()
    .replace(/[sp{Punctuation}]+/gu, "-")
    .replace(/^-+|-+$/g, "");
}
console.log(toSlugJP("JavaScript 正規表現 完全ガイド!"));
// "javascript-正規表現-完全ガイド"

14. ログ解析と CSV パース

サーバーログや CSV は正規表現でこそ綺麗にパースできるテキスト形式です。matchAll と名前付きグループの相性が抜群です。

14.1 Apache / Nginx アクセスログ

標準的なアクセスログは固定のフィールド順になっているので、名前付きグループでマッピングするとそのままオブジェクトになります。

const log = '192.168.0.1 - - [27/May/2026:12:34:56 +0900] "GET /index.html HTTP/1.1" 200 1234 "-" "Mozilla/5.0"';

const LOG_RE = /^(?S+) S+ S+ [(?

14.2 CSV の素朴なパース

クォート対応の CSV パースは正規表現だけだとややしんどいですが、「カンマ区切り、ダブルクォート対応」までは正規表現で書けるレベルです。本格的にはライブラリ推奨。

// CSV 1行のパース(クォート対応)
function parseCsvLine(line) {
  const cells = [];
  const re = /(?:^|,)(?:"([^"]*)"|([^,]*))/g;
  for (const m of line.matchAll(re)) {
    cells.push(m[1] ?? m[2] ?? "");
  }
  return cells;
}

console.log(parseCsvLine('a,b,c'));                  // ["a","b","c"]
console.log(parseCsvLine('a,"b,c",d'));              // ["a","b,c","d"]
console.log(parseCsvLine('"hello",world,"foo bar"'));// ["hello","world","foo bar"]

14.3 マークダウンの簡易パース

軽量なマークダウン要素抽出も正規表現の出番です。本格的なパーサーには負けますが、シンプルな置換なら十分実用になります。

// 軽量マークダウン→HTML
function md(text) {
  return text
    .replace(/^### (.+)$/gm, "

") .replace(/^## (.+)$/gm, "

") .replace(/^# (.+)$/gm, "

$1

") .replace(/**(.+?)**/g, "$1") .replace(/*(.+?)*/g, "$1") .replace(/`(.+?)`/g, "$1") .replace(/[(.+?)]((.+?))/g, '
$1'); } console.log(md("# Hellon**bold** and *italic* and `code` and [link](https://x.com)")); // "

Hello

nbold and italic and code and link"

15. ReDoS 対策とパフォーマンス

正規表現は強力ですが、「壊滅的バックトラッキング」と呼ばれる病的な遅さを引き起こすパターンがあります。これが ReDoS(Regular Expression Denial of Service)。ユーザー入力を正規表現に流す API を書くなら、必ず対策しましょう。

15.1 ReDoS の典型例

「量指定子が二重にネストしている」パターンが ReDoS の温床です。(a+)+(a*)* のような書き方は危険です。

// ❌ 危険なパターン:(a+)+
function unsafeCheck(s) {
  return /^(a+)+$/.test(s);
}

// 短い入力は問題なし
console.log(unsafeCheck("aaaa")); // true(一瞬)

// 失敗パターンで指数関数的に遅くなる
// "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" のような入力で固まる
// (実行注意:数十秒〜数分かかる)
// console.log(unsafeCheck("aaaaaaaaaaaaaaaaaaaaaaaaaaaab"));

15.2 安全な書き換えパターン

ネストした量指定子は非キャプチャ + atomic 風書き換えで改善できます。JavaScript には atomic group がないので、lookahead を使った「possessive 化」テクニックを使います。

// ✅ 安全な書き換え:量指定子をネストしない
function safeCheck(s) {
  return /^a+$/.test(s);
}
console.log(safeCheck("aaaa"));                          // true
console.log(safeCheck("aaaaaaaaaaaaaaaaaaaaaaaaaaaab")); // false(一瞬)

// 古典:HTML 属性パターンの ReDoS 例
// 危険: /]*href="(.+)"[^>]*>/
// 改善: 文字クラスを厳密化して非バックトラックに
const SAFE_HREF = /]*?href="([^"]*)"/;
console.log(SAFE_HREF.exec(''));
// ["...", "https://x.com"]

15.3 タイムアウト付き実行

JavaScript 標準には正規表現のタイムアウトがないので、Worker やサブプロセスで実行して時間制限を掛けるか、入力長を制限するのが現実解です。

// 入力長で防御
function safeTest(re, input, maxLen = 1000) {
  if (typeof input !== "string") return false;
  if (input.length > maxLen) {
    throw new Error(`Input too long: ${input.length} > ${maxLen}`);
  }
  return re.test(input);
}

console.log(safeTest(/^a+$/, "aaaa"));            // true
// safeTest(/^a+$/, "a".repeat(10000));            // throws

// Node 22+:正規表現は通常入力長でリニアになるよう改善されているが、
// ユーザー入力の正規表現は実行前に入力長をチェックする習慣を。

15.4 ベンチマークの取り方

正規表現の遅さは「型推論できない悪い書き方」と「入力サイズ依存」の 2 種類です。console.timeperformance.now() で実測しましょう。

// ベンチマーク
function bench(label, fn, iter = 10_000) {
  const t0 = performance.now();
  for (let i = 0; i  /a*b/.test(text));
bench("lazy",   () => /a*?b/.test(text));
bench("safer",  () => /[^b]*b/.test(text));

// 典型的な結果:lazy が最速、safer もほぼ同等、greedy はやや遅い

16. TypeScript Template Literal Types との連携

TypeScript の Template Literal Types と正規表現は、文字列操作の両輪です。型レベルで文字列パターンを表現し、ランタイムでは正規表現で検証する組み合わせが理想形です。

16.1 型ガードとしての正規表現

Template Literal Types で表現した型に対して、正規表現で検証する型ガード関数を作ると、ランタイムとコンパイル時の両方で型安全になります。

// TypeScript
type HexColor = `#${string}`;
type ISODate = `${number}-${number}-${number}`;
type Email = `${string}@${string}.${string}`;

const HEX_RE = /^#[0-9a-fA-F]{6}$/;
const DATE_RE = /^d{4}-d{2}-d{2}$/;
const EMAIL_RE = /^[^s@]+@[^s@]+.[^s@]+$/;

function isHexColor(s: string): s is HexColor {
  return HEX_RE.test(s);
}
function isISODate(s: string): s is ISODate {
  return DATE_RE.test(s);
}
function isEmail(s: string): s is Email {
  return EMAIL_RE.test(s);
}

const input: string = "#ff8800";
if (isHexColor(input)) {
  // input は HexColor 型に絞り込まれる
  console.log(input.toUpperCase());
}

16.2 ブランド型と組み合わせる

より強い型安全にはブランド型(branded types)と組み合わせます。検証を通った文字列だけがブランド付きの型になり、検証なしで使えなくなります。

// TypeScript
type Branded = T & { __brand: B };
type EmailAddress = Branded;

function parseEmail(s: string): EmailAddress {
  if (!/^[^s@]+@[^s@]+.[^s@]+$/.test(s)) {
    throw new Error(`Invalid email: ${s}`);
  }
  return s as EmailAddress;
}

function sendMail(to: EmailAddress, body: string): void {
  console.log(`Sending to ${to}: ${body}`);
}

// sendMail("foo@example.com", "hi");  // ❌ コンパイルエラー
sendMail(parseEmail("foo@example.com"), "hi");  // ✅

16.3 名前付きグループの型推論

TypeScript 5.x では 正規表現リテラルから名前付きグループの型が推論できます。groups オブジェクトのキーがリテラル型として効きます。

// TypeScript
const re = /(?d{4})-(?d{2})-(?d{2})/;
const m = re.exec("2026-05-27");

if (m?.groups) {
  // m.groups は { year: string; month: string; day: string }
  const { year, month, day } = m.groups;
  console.log(year, month, day);
}

// 注意:tsconfig の noUncheckedIndexedAccess の有無で groups の型挙動が変わる

17. デバッグツールとテスト戦略

正規表現は 「動いた」だけで満足してはいけません。エッジケースのテストとビジュアル化が不可欠です。

17.1 regex101 と Debuggex

regex101.comパターンの各部分が何をしているかをステップ表示してくれる定番ツールです。Flavor を「ECMAScript (JavaScript)」に切り替えるのを忘れずに。Debuggex は状態遷移図として可視化してくれます。

// regex101 で確認するときのコツ
// 1. Flavor を "ECMAScript (JavaScript)" に
// 2. Test string にエッジケースを並べる
// 3. Explanation パネルで意味を確認
// 4. Code Generator で他言語版もすぐ取れる

// 開発中はソースに URL を残しておくと未来の自分が助かる
// https://regex101.com/r/xxxxxx/1
const DATE_RE = /^(?d{4})-(?0[1-9]|1[0-2])-(?0[1-9]|[12]d|3[01])$/;

17.2 テストケース駆動で正規表現を書く

正規表現は「正例」と「反例」のテストケースを並べてから書くのが鉄則です。先にテストを並べると、漏れや過剰マッチに気付きやすくなります。

// テストケース駆動(Vitest 想定)
import { describe, it, expect } from "vitest";

const EMAIL = /^[^s@]+@[^s@]+.[^s@]+$/;

describe("EMAIL", () => {
  it.each([
    "foo@example.com",
    "foo.bar+tag@sub.example.co.jp",
    "a@b.c",
  ])("accepts %s", (s) => {
    expect(EMAIL.test(s)).toBe(true);
  });

  it.each([
    "",
    "foo",
    "foo@",
    "@example.com",
    "foo@bar",
    "foo @example.com",
  ])("rejects %s", (s) => {
    expect(EMAIL.test(s)).toBe(false);
  });
});

17.3 デバッグ用ヘルパー関数

マッチ結果を見やすく整形するヘルパー関数を 1 つ持っておくと、デバッグが圧倒的に楽になります。

// マッチ結果整形
function debugMatch(re, input) {
  console.log(`Pattern: ${re}`);
  console.log(`Input:   ${JSON.stringify(input)}`);

  if (re.global) {
    const all = [...input.matchAll(re)];
    console.log(`Matches: ${all.length}`);
    all.forEach((m, i) => {
      console.log(`  [${i}] "${m[0]}" @ ${m.index}`);
      if (m.groups) console.log("    groups:", m.groups);
    });
  } else {
    const m = re.exec(input);
    if (!m) {
      console.log("No match");
    } else {
      console.log(`Match: "${m[0]}" @ ${m.index}`);
      m.forEach((g, i) => i > 0 && console.log(`  [${i}] "${g}"`));
      if (m.groups) console.log("  groups:", m.groups);
    }
  }
}

debugMatch(/(?d{4})-(?d{2})/g, "2026-05, 2025-12");
// Pattern: /(?d{4})-(?d{2})/g
// Input:   "2026-05, 2025-12"
// Matches: 2
//   [0] "2026-05" @ 0
//     groups: { y: "2026", m: "05" }
//   [1] "2025-12" @ 9
//     groups: { y: "2025", m: "12" }

18. よくあるハマりどころとアンチパターン

最後に、本番コードでよく見かけるアンチパターンと回避策をまとめます。これだけ押さえておけば、レビューで指摘されることは大幅に減ります。

18.1 g フラグ + test の lastIndex 罠

g フラグ付き正規表現を test でループ呼びすると、lastIndex が進むため毎回結果がブレます。同じ入力に何度も test するなら、g を外すか、lastIndex = 0 に明示リセットすべきです。

// ❌ よくあるバグ
const re = /d+/g;
console.log(re.test("123")); // true
console.log(re.test("123")); // false(lastIndex が末尾なのでマッチしない)
console.log(re.test("123")); // true(リセットされて戻る)

// ✅ 修正案1:g を外す
const re2 = /d+/;
console.log(re2.test("123")); // true
console.log(re2.test("123")); // true

// ✅ 修正案2:毎回リセット
function reset(re, s) {
  re.lastIndex = 0;
  return re.test(s);
}

18.2 HTML を正規表現でパースしようとしない

「HTML を正規表現でパースするな」は古典的な助言ですが、いまだに守られていません。複雑な HTML には DOMParsercheerio を使い、正規表現は「単一タグの軽い抽出」程度に留めるのが現実的です。

// ❌ HTML を正規表現で完全パース(壊れる)
const html = '
foo bar
'; console.log(html.match(/(.*?)/)); // ネストや属性のバリエーションで簡単に破綻する // ✅ DOMParser(ブラウザ) const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); doc.querySelectorAll("a").forEach((a) => console.log(a.href, a.textContent)); // ✅ 軽い抽出だけなら正規表現 OK const links = [...html.matchAll(/href="([^"]+)"/g)].map(m => m[1]); console.log(links); // ["x"]

18.3 文字列リテラル内のバックスラッシュ

new RegExp(string) の文字列内で バックスラッシュをエスケープし忘れるのが頻発バグです。リテラル記法ならエスケープ不要なので、可能ならリテラルを使いましょう。

// ❌ ありがち
const re1 = new RegExp("d+");  // 警告:不正なエスケープ。実際は /d+/ になる
console.log(re1.test("123"));    // false

// ✅ 正しい
const re2 = new RegExp("\d+");
console.log(re2.test("123"));    // true

// ✅ もっと良い(リテラル)
const re3 = /d+/;
console.log(re3.test("123"));    // true

18.4 まとめ:正規表現を書く順序

最後に、正規表現を書くときの推奨手順をまとめます。この順番を守るだけでバグはほぼ消えます。

// 推奨手順
// 1. テストケース(正例・反例)を列挙する
// 2. regex101 でスケッチして動作確認
// 3. 名前付きグループで保守性を上げる
// 4. ユーザー入力は escapeRegExp してから流す
// 5. 量指定子はネストしない(ReDoS 防止)
// 6. lookahead/lookbehind は必要な部分だけ
// 7. 単体テストを書いてリグレッションを防ぐ
// 8. 複雑になったら別関数+コメントに切り出す

const PATTERNS = {
  // 数値(整数)
  INTEGER: /^-?d+$/,
  // 数値(小数含む)
  NUMBER: /^-?d+(?:.d+)?$/,
  // ULID
  ULID: /^[0-9A-HJKMNP-TV-Z]{26}$/,
  // UUID v4
  UUID_V4: /^[da-f]{8}-[da-f]{4}-4[da-f]{3}-[89ab][da-f]{3}-[da-f]{12}$/i,
  // セマンティックバージョン
  SEMVER: /^(d+).(d+).(d+)(?:-([w.-]+))?(?:+([w.-]+))?$/,
};

console.log(PATTERNS.ULID.test("01ARZ3NDEKTSV4RRFFQ69G5FAV"));  // true
console.log(PATTERNS.UUID_V4.test("550e8400-e29b-41d4-a716-446655440000"));// true
console.log(PATTERNS.SEMVER.test("1.2.3-beta.1+build.42"));               // true

19. まとめ:正規表現は型より先に身につける投資

JavaScript の正規表現は、「分かれば一気に世界が広がる」が「分からないと一生避けて通る」典型的な技術です。本記事で扱った範囲を整理しておきます。

  • リテラル vs new RegExp():固定パターンはリテラル、動的はコンストラクタ + escapeRegExp
  • test / exec / match / matchAll / replace / split:用途で使い分け。matchAll が現代の主役
  • フラグ8種:g/i/m/s/u/y/d/v。日本語を扱うなら u は必須、ES2024 の v も使いこなしたい
  • 名前付きグループ + lookbehind:ES2018 以降の現代正規表現の核
  • Unicode property:日本語・絵文字を正しく扱う唯一の方法
  • 実用パターン:メール・URL・日付・カード番号は形式チェック+別ロジック検証の組み合わせで
  • ReDoS 対策:量指定子のネストを避け、入力長を制限する
  • TypeScript 連携:Template Literal Types + 正規表現の型ガードで二重に守る

正規表現が読める・書けるエンジニアは、テキスト処理に費やす時間が他人の半分以下になります。今後の関連トピックとしては、Template Literal Types による型レベル文字列操作、配列メソッド との合わせ技、そして Node.js でのストリーム処理との連携が深掘り対象になるはずです。

正規表現はバージョンアップで地味に進化を続けています。次に v フラグの集合演算をプロジェクトで使うとき、本記事を再度開いてください。きっと役に立ちます。

コメント

タイトルとURLをコピーしました