電話番号の正規表現(JavaScript)|ハイフン有無・全角・+81を判定する実用パターン
フォームの電話番号欄を作るとき、「ハイフンあり・なしの両方を許可したい」「全角で入力されても通したい」「携帯か固定かも判定したい」と要件が増えるほど、正規表現が一本では書けなくなります。
結論から言うと、1本の巨大な正規表現で判定しようとせず、「数字だけに正規化する → 桁数と先頭で種別を判定する」の2段階に分けるのが最も壊れにくい方法です。この記事では Node.js v20.20.1 で実際に動かした結果をもとに、そのコードと、見落としやすい落とし穴を整理します。ブラウザ各種や他バージョンの Node.js は検証していません。
結論:先に正規化し、あとから判定する
使うコードは次のとおりです。
// 1段階目:入力を「数字だけ」にする(不正な文字が混ざっていれば null)
function toDigits(input) {
let s = input.normalize("NFKC").trim();
// 数字・空白・括弧・ドット・プラス・各種ハイフン以外が混ざっていたら不正
if (!/^[\d\s().+\-‐-―−ー]+$/.test(s)) return null;
const intl = /^\+81/.test(s);
if (intl) s = s.replace(/^\+81\s*(?:\(0\))?/, ""); // +81 と (0) を外す
s = s.replace(/\D/g, ""); // 数字以外をすべて削除
return intl ? "0" + s : s;
}
// 2段階目:数字だけの文字列から種別を判定する
function classify(d) {
if (d === null) return null;
if (/^0[789]0\d{8}$/.test(d)) return "mobile"; // 携帯 11桁
if (/^050\d{8}$/.test(d)) return "ip"; // IP電話 11桁
if (/^0120\d{6}$/.test(d) || /^0800\d{7}$/.test(d)) return "freedial"; // フリーダイヤル
if (/^0570\d{6}$/.test(d)) return "navidial"; // ナビダイヤル 10桁
if (/^0(?![789]0|50)[1-9]\d{8}$/.test(d)) return "fixed"; // 固定電話 10桁
return null;
}
実行結果(実測)です。
| 入力 | toDigits の結果 | classify |
|---|---|---|
090-1234-5678 | 09012345678 | mobile |
090‐1234‐5678(U+2010) | 09012345678 | mobile |
090ー1234ー5678(長音) | 09012345678 | mobile |
(03)1234-5678 | 0312345678 | fixed |
+81 (0)90-1234-5678 | 09012345678 | mobile |
0120-123-456 | 0120123456 | freedial |
090abc12345678 | null | null |
番号の種別(桁数と先頭の並び)は日本で一般に知られている番号体系にもとづいています。実際の運用では、総務省の番号体系の資料で最新の区分を確認してください。この記事のコードは書式のチェックであり、番号が実在するか・つながるかは判定できません。
理由1:ハイフンありなしを1本の正規表現にすると誤判定が増える
よく見かけるのは、ハイフンを任意にした次のようなパターンです。
const loose = /^0\d{1,4}-?\d{1,4}-?\d{4}$/;
これを実際に試すと、通ってほしくないものまで通ります。
| 入力 | 結果 | コメント |
|---|---|---|
03-1234-5678 | true | 正しい |
0312345678 | true | 正しい |
00-0000-0000 | true | 先頭が 00 の番号は国内番号ではないはず |
03-12345678 | true | ハイフンの位置が不揃い |
0312-345678 | true | 同上 |
0120-123-456 | false | フリーダイヤルが落ちる |
最後の行が実務では厄介です。フリーダイヤルは 0120-123-456 のように末尾が3桁で区切られるため、「末尾4桁」を前提にした式では弾かれます。当サイトの正規表現テスターに入っている「電話番号(日本)」のプリセットも 0\d{1,4}-\d{1,4}-\d{4} で、ハイフンが必須です。プリセットは試し書きの出発点であり、そのままフォームの本番検証に使うものではありません。
ハイフン位置は市外局番の桁数(2〜5桁)で変わるため、位置まで正規表現で検証するのは割に合いません。位置は検証せず、数字の並びだけ見るほうが堅実です。
理由2:\d は全角数字にマッチしない
全角で入力された 090 は、人間には電話番号に見えても、JavaScript の \d は反応しません。
/\d/.test("0"); // false
/\d/u.test("0"); // false(u フラグを付けても同じ)
\d は ASCII の 0-9 だけです。そのため、検証の前に全角を半角へ揃える工程が必要です。全角・半角の変換全般は全角・半角が混在したデータを一括で揃える方法で、半角カナ側の落とし穴は半角カナを全角に変換するJavaScriptでまとめています。
落とし穴1:normalize("NFKC") だけではハイフンが揃わない
全角を半角にするには normalize("NFKC") が手軽で、全角数字や全角スペースは期待どおりになります。ところがハイフンに見える文字は NFKC ですべて - になるわけではありません。実測した結果が次のとおりです。
| 文字 | コードポイント | NFKC 後に - になるか |
|---|---|---|
-(半角) | U+002D | true |
-(全角ハイフンマイナス) | U+FF0D | true |
﹣(小さいハイフンマイナス) | U+FE63 | true |
‐(ハイフン) | U+2010 | false(そのまま) |
−(マイナス記号) | U+2212 | false |
―(ホリゾンタルバー) | U+2015 | false |
ー(長音) | U+30FC | false |
ー(半角の長音) | U+FF70 | 長音 ー になるだけ |
スマホの日本語キーボードやコピー元のPDF・Webページによって、入力されるハイフンの種類はまちまちです。NFKC のあとに /^\d{3}-\d{4}$/ のように - だけを前提にすると、見た目は正しいのに弾かれる入力が出ます。
対策は2通りです。
- ハイフン類を全部
-に置換してから検証する(置換対象の列挙漏れが起きやすい) - ハイフン類を区別せず、数字以外をすべて削除してから検証する(上のコードの方法)
2番を選ぶと、ハイフンの種類を気にする必要がなくなります。ただし、数字以外を無条件に消すと 090abc12345678 のような文字列も 09012345678 になって通ってしまいます。そのため、冒頭のコードでは「許可する文字の集合」に収まっているかを先に確認し、外れたら null を返しています(実測で 090abc12345678 は null)。
落とし穴2:+81 (0)90 を単純に置換すると0が二重になる
海外からの入力や名刺由来のデータでは、+81 90-1234-5678 や +81 (0)90-1234-5678 のような国際表記が入ってきます。国際表記では先頭の0を省くのが基本ですが、「(0)」を括弧付きで残す書き方もあります。
+81 を 0 に置き換える単純な実装を、数字だけにしたあとで当てると、次のようになります。
digits("+81 (0)90-1234-5678") // "+8109012345678"
digits(...).replace(/^\+81/, "0") // "009012345678"(0が二重)
実測で 009012345678 になり、携帯の判定に落ちます。上のコードは、数字化する前に +81 と (0) をまとめて外し、最後に1つだけ 0 を付けています。+81 90-1234-5678 も +81 (0)90-1234-5678 も 09012345678 に揃うことを確認しました。
落とし穴3:先頭が 090 の10桁が「固定電話」として通る
種別判定でありがちなのが、固定電話を ^0[1-9]\d{8}$(0+1〜9+8桁=10桁)と書くパターンです。これだと、携帯の桁が1つ足りない入力が固定電話になります。
/^0[1-9]\d{8}$/.test("0901234567"); // true(090-1234-567:携帯の末尾が1桁足りない入力)
入力ミスで携帯番号の最後の1桁が欠けただけなのに、「固定電話として有効」と判定されてしまいます。冒頭のコードでは、固定電話の式に (?![789]0|50) を足して、070 080 090 050 で始まる10桁を除外しました。
/^0(?![789]0|50)[1-9]\d{8}$/.test("0901234567"); // false
/^0(?![789]0|50)[1-9]\d{8}$/.test("0921234567"); // true(092 は固定電話の市外局番)
/^0(?![789]0|50)[1-9]\d{8}$/.test("0721234567"); // true
072 や 092 のように、先頭が 07 09 でも固定電話の市外局番になる番号は通しています。「0から始まって7〜9だから携帯」と雑に除外すると、こうした正しい固定電話まで落とすので注意してください。
また、0120(フリーダイヤル)と 0570(ナビダイヤル)は10桁で、固定電話の式にも当てはまります。判定の順番を「特殊番号 → 固定電話」にしないと、フリーダイヤルが固定電話に分類される点も、実測で重複を確認しています。
落とし穴4:type="number" と Excel で先頭の0が消える
入力欄を <input type="number"> にするのは避けてください。数値として扱われるため、先頭の0が意味を持たなくなります。
Number("09012345678"); // 9012345678(先頭の0が消える)
parseInt("09012345678", 10); // 9012345678
電話番号は計算に使わない「数字の形をした文字列」です。電話番号欄は type="tel" にし、値は常に文字列として保持します。なお type="tel" は書式を検証してくれるわけではないので、検証は自前の処理で行います(スマホで数字キーボードを出す効果が主な用途です)。
保存後に CSV を Excel で開いて0が落ちる問題は、CSVをExcelで開くと0が消える・桁が変わるで原因と対処を整理しています。
厳しくしすぎない:判定の目的を決める
電話番号の書式チェックは、メールアドレスと同じく厳しくするほど本物の入力を弾く性質があります。方針はメールアドレスの正規表現と同じです。
| 目的 | 推奨する判定 |
|---|---|
| 入力ミスの検出だけでよい | 数字化して10桁か11桁で、先頭が0 |
| 携帯番号を必須にしたい(SMS認証など) | ^0[789]0\d{8}$ のみ |
| 固定・携帯・フリーダイヤルを区別して保存したい | 上記の classify |
| 海外番号も受けたい | 自前の正規表現ではなく専用ライブラリ(例:libphonenumber系)を検討 |
専用ライブラリは今回の検証では動かしていません。導入する場合は、バンドルサイズと日本以外の番号に対応する必要があるかを見て判断してください。
また、SMS認証が目的なら、正規表現でいくら厳しく確認しても実際にSMSが届くかは分かりません。本当に確認したい場合は、確認コードを送る運用に任せるのが確実です。
保存時にハイフンを付けたい場合
画面表示用にハイフンを付けるなら、保存は数字のみ、表示時に整形するのが安全です。携帯・03・06 のように桁区切りが決まっているものだけを整形する簡易例です。
function format(d) {
if (/^0[789]0\d{8}$/.test(d)) return d.replace(/^(\d{3})(\d{4})(\d{4})$/, "$1-$2-$3");
if (/^0[36]\d{8}$/.test(d)) return d.replace(/^(\d{2})(\d{4})(\d{4})$/, "$1-$2-$3");
return d; // それ以外は整形せずそのまま返す
}
format("09012345678"); // "090-1234-5678"
format("0312345678"); // "03-1234-5678"
format("0451234567"); // "0451234567"(整形対象外)
固定電話は市外局番の桁数が地域ごとに異なり(例えば 045 は3桁、0123 は4桁など)、全国分をきれいに区切るには市外局番の一覧表が必要です。上の例が 0451234567 を整形しないのはそのためで、自前でやりきろうとせず、整形できないものは数字のまま返す割り切りをおすすめします。
まとめ
- 電話番号の検証は、数字だけに正規化してから桁数と先頭で判定する2段階にすると壊れにくい
\dは全角数字にマッチしない。normalize("NFKC")でも‐−―ーは-にならないため、数字以外をすべて削除する方式が確実+81 (0)90は数字化の前に外さないと00…になる。固定電話の式は090などの10桁を通さないよう除外する- 入力欄は
type="tel"にし、値は文字列で扱う。type="number"は先頭の0が消える - 書式チェックは入力ミスの検出が目的。実在の確認はできないので、必要なら確認コードの送信に任せる
試し書きには正規表現テスター、入力データの全角・半角の統一には全角⇔半角変換が使えます。
なお、本記事の挙動は Node.js v20.20.1 で確認したもので、ブラウザや他バージョンでの結果は検証していません。番号体系の最新の区分は、総務省の公開資料などで確認してください。