この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。MDNとUnicodeの一次資料を確認し、架空文字列を使ったJavaScriptの書記素カウンターを制作しました。検証ステータス:📘 公式仕様確認・V8ロジック9項目PASS/ブラウザー実表示・GitHub Pages公開URL・WordPress実表示は未確認
「10文字まで」と書いた入力欄で、家族の絵文字「👨👩👧👦」を入れると、JavaScriptでは11という数字が出ることがあります。画面には一つの絵文字に見えるのに、なぜでしょうか。実は「文字数」という言葉には、UTF-16コード単位、Unicodeコードポイント、書記素クラスターという異なる数え方があります。どれが正しいかは、フォームで何を制限したいかによって変わります。 、まず架空の入力だけを扱う無料の小さな教材を動かし、三つの数字を並べて観察します。ライブラリーのインストールや外部APIの登録は不要です。実務では「表示上の10文字」と「データベースに保存できる長さ」が一致するとは限らないので、見た目のカウンターと保存側の検証を分ける考え方まで確認します。
今回できること
| 項目 | 内容 |
|---|---|
| 対象 | Intl.Segmenter対応ブラウザー、Node.js 22 |
| 費用と通信 | CDN・APIキー不要。教材は入力を送信・保存しない |
| 入力 | 日本語、😀、家族絵文字、結合アクセントなどの架空文字 |
| まず試す | 家族の絵文字ボタン→日本語ボタン |
| 検証 | V8で9項目PASS。実ブラウザー画面は未確認 |
まず動かしてみる
Daily CodeのHTML・JavaScript・READMEに完成版とテストがあります。READMEのローカル起動手順から始めてください。GitHub Pagesの想定デモURLも示しますが、このURLの到達とブラウザーでの実動作は未確認です。開けない場合は故障と即断せず、GitHub上のファイルとローカル起動で切り分けます。
リポジトリのルートで
python3 -m http.server 8000を起動し、http://127.0.0.1:8000/web/grapheme-counter/を開きます。追加パッケージは不要です。最初の家族絵文字の三つの数値を見ます。UTF-16/コードポイント/書記素は11/7/1が期待値です。
「日本語(基準)」を押すと3/3/3が期待値です。続いて「11個の英字」を押すと11/11/11となり、書記素10個の警告が出る想定です。
ここで大切なのは、数字が変わる理由です。三つの欄は同じ文字列を見ていますが、同じ単位を数えていません。「家族の絵文字が11文字ある」という説明は、UTF-16単位数を表すなら正確でも、利用者が画面で認識する文字のまとまりの説明としては誤解を招きます。反対に、書記素数だけを示しても、保存先のバイト制限を守れるとは限りません。
どうして11・7・1になるのか
String.lengthはJavaScript内部のUTF-16コード単位を数えます。一般的な日本語文字は多くの場合1単位ですが、一部の絵文字はサロゲートペアとして2単位を使います。一方、Array.from(text)は文字列のイテレーターを通してコードポイントごとに扱います。家族の絵文字は複数の人物絵文字とゼロ幅接合子(ZWJ)からなるため、コードポイントでも一つにはなりません。
Intl.Segmenter("ja", { granularity: "grapheme" })は、Unicodeの書記素境界に従って文字列を分割するための標準APIです。家族の絵文字のような結合された並びを、一つの書記素クラスターとして数えることができます。ただし「書記素」は人間が必ず一文字と感じる単位を完全に再現する保証ではありません。表示フォント、Unicodeの版、言語や製品独自のルールとは区別する必要があります。
flowchart LR
A["架空の入力文字列"] --> B["String.length"]
A --> C["Array.from"]
A --> D["Intl.Segmenter"]
B --> E["UTF-16単位数"]
C --> F["コードポイント数"]
D --> G["書記素クラスター数"]
G --> H["UI上の10個警告"]
この図の最後にある警告は、教材の表示だけを変えるものです。サーバーへ送信する前の検証や、保存先の制約まで保証する仕組みではありません。JavaScriptが無効な環境や意図的にリクエストを書き換えた場合も想定し、実サービスではサーバー側の検査を省略しないでください。
コードの大事なところ
function measure(text, Segmenter = Intl.Segmenter) {
const utf16 = text.length;
const codePoints = Array.from(text).length;
const graphemes = typeof Segmenter === "function"
? Array.from(new Segmenter("ja", { granularity: "grapheme" }).segment(text)).length
: null;
return { utf16, codePoints, graphemes };
}
typeof Segmenter === "function"で対応状況を調べるのは、古いブラウザーで書記素数を推測値に置き換えないためです。未対応ならnullを返し、画面では「未対応」と表示します。コードポイント数を勝手に書記素数として扱うと、結合文字やZWJ絵文字で誤った残り文字数を表示してしまいます。対応ブラウザーを要件にするか、検証済みの代替ライブラリーを採用するかは、サービス側の判断が必要です。
完成版ではinputイベントを使い、入力欄の内容が変わるたびに三つの値を更新します。例ボタンを押した場合も同じ描画関数を呼び出します。表示先にはtextContentを使用しているため、入力文字列をHTMLとして解釈させません。入力した文字列をネットワークへ送信したり、ブラウザーのストレージへ保存したりする処理もありません。実業務の入力フォームに移す際は、これらの安全条件を崩さないようにしてください。
一つだけ条件を変えて比較する
| 条件 | UTF-16単位 | コードポイント | 書記素 |
|---|---|---|---|
| 日本語 | 3 | 3 | 3 |
| 😀 | 2 | 1 | 1 |
| 👨👩👧👦 | 11 | 7 | 1 |
| e+結合アクセント | 2 | 2 | 1 |
| ABCDEFGHIJK | 11 | 11 | 11 |
まず「日本語」で三つの数が一致することを確認し、入力だけを家族の絵文字に変更してください。もしlengthしか使っていなければ、画面上は一つに見える入力に対して「11文字」と判定します。逆に、11個の英字なら三つの方法が一致するので、10書記素の警告が出ます。この違いから、何を上限にしたいのかという要件を先に決める必要があると分かります。
もう一つの観察対象が、eの後ろに結合アクセントを置いた文字列です。コードポイントは二つですが、書記素としては一つになります。絵文字だけをテストして「Array.fromなら十分」と結論づけないことが大切です。全角・半角や見た目の横幅を測りたい場合も、書記素数とは別の指標が必要です。
うまく動かないとき
| 症状 | まず確認 | 安全な対処 |
|---|---|---|
| 書記素欄が未対応 | Consoleでtypeof Intl.Segmenter | 対応環境を使う。代替値を正解と偽らない |
| ボタンで数字が変わらない | Networkでapp.js取得、Consoleのエラー | READMEのローカルHTTP起動を試す |
| デモURLが開かない | Pagesの公開設定とGitソースの存在 | 公開URLの故障と即断しない |
| 保存時に長さエラー | 保存先のバイト・コード単位・書記素の定義 | UIとサーバーの制約を別々に確認 |
この教材では例ボタンとテキストエリアにラベルを付け、数値の変化をテキストで示します。色だけで合否を知らせず、状態欄に文章を表示します。ただし、スクリーンリーダーやスマートフォンの実機試験は行っていないため、アクセシビリティを完全に保証するものではありません。業務画面へ組み込む前には、キーボード操作や読み上げの頻度も確認してください。
仕事でどう使う?
商品名、SNSの下書き、ニックネーム、問い合わせフォームなどでは、「残り何文字」と表示するUIが役立ちます。ただし、仕様書に「10文字以内」とだけ書くと、担当者によってlength、コードポイント、書記素を別々に選ぶ可能性があります。利用者が画面で扱うまとまりを優先するなら書記素数を候補にし、外部APIがUTF-8バイト数を制限するならTextEncoderなどによる別の測定が必要です。二つを混同しない設計が、入力拒否や文字化けの問い合わせを減らす出発点になります。
例えば架空の「イベント名は画面上10文字以内、保存先はUTF-8で40バイト以内」という要件なら、入力中の表示は書記素数、送信前とサーバー側の保存判定はバイト数というように、別のチェックが必要です。クライアント側で書記素10個以内でも、サーバー側の40バイトを超える入力はあり得ます。だからこそ、単一のカウンターを「どんな文字でも正確」と宣伝するのではなく、画面の目的と保存の目的を分けて説明することが重要です。
AIが提案したコードを採用する場合も同じです。生成されたコードがtext.lengthだけを使っていたら、すぐ不正解と断定せず「何を数える仕様か」を確かめます。仕様が書記素なら家族絵文字と結合アクセントを試し、仕様がUTF-16ならlengthが適切なこともあります。検算の観点を人間が決めることが、生成AIを安全に使う実務上のポイントです。今回はCopilotの実回答や各製品の動作を検証したものではありません。
検証範囲と注意
固定コミットのJavaScriptロジックをV8で9項目検査し、ASCII、日本語、絵文字、家族のZWJ絵文字、結合文字、混在、空文字、11文字、未対応時の戻り値を確認しました。実ブラウザーのDOM操作、公開GitHub Pagesの到達、WordPressの青い引用囲み、実スマートフォン、印刷、読み上げは未確認です。記事の図は処理の概念図であり、WordPressでのMermaid描画も未確認です。独立アイキャッチは作成・登録しておらず、通常記事の既存資産経路を使う方針です。実在しない画像URLは載せません。
公式情報・参考資料
MDN: Intl.Segmenter(2026-11-02確認)
MDN: String.length(2026-11-02確認)
Unicode Consortium: UAX #29(2026-11-02確認)
Daily Code: 固定コミットのREADME(今回の教材とテスト)
