【テクニカル・上級編】TypeScriptのプリミティブ型における「小文字」と「大文字」の非対称性:なぜStringを使ってはいけないのか – TypeScript コア・型システムの基礎解析バイブル

TypeScriptのプリミティブ型における「小文字」と「大文字」の非対称性:なぜ `String` を使ってはいけないのか

TypeScriptのコードベースを監査していると、いまだに `name: String` や `id: Number` といった型注釈を見かけることがある。C#やJavaなどの静的型付け言語をバックグラウンドに持つエンジニアが書きがちなアンチパターンだが、TypeScript(そしてその背後にあるJavaScriptのランタイム)において、小文字のプリミティブ型 (`string`) と大文字のラッパーオブジェクト型 (`String`) は、意味論的にも、型システムの安全性においても、そしてメモリ・実行性能の観点からも全く異なる代物である。

今回は、コンパイラが型をどう評価し、V8などのJSエンジンがメモリ上でどう処理しているのか、その低レイヤの挙動に踏み込んで「なぜ `String` を使ってはいけないのか」を完全解明する。

—

1. 型システムの罠:`String` は「文字列」ではない

まずは、TypeScriptの型定義ファイル(`lib.d.ts`)を覗いてみよう。大文字の `String` は、JavaScriptの標準組み込みオブジェクトである `String` コンストラクタのプロトタイプチェーンそのものを指している。

// lib.d.ts の概念的な定義
interface String {
length: number;
charAt(pos: number): string;
charCodeAt(pos: number): number;
// …膨大なメソッド群
}

interface StringConstructor {
new (value?: any): String;
(value?: any): string;
readonly prototype: String;
// …
}
declare var String: StringConstructor;

ここで重要なのは、`String`(大文字)という型は 「プリミティブな文字列」だけでなく「`new String(“…”)` で生成されたラッパーオブジェクト」も含んでしまう という点だ。

以下のコードを見てほしい。

const primitiveStr: string = “hello”;
const objectStr: String = new String(“hello”);

// string 型に String オブジェクトは代入できるか?
const a: string = objectStr;
// ❌ コンパイルエラー!
// Type ‘String’ is not assignable to type ‘string’.
// Type ‘String’ is not a primitive, but is a wrapper object. Use ‘string’ when possible.

// 逆に、String 型に string は代入できるか?
const b: String = primitiveStr;
// ✅ 通ってしまう!

なぜ `string` に `String` が代入できないのか。TypeScriptコンパイラは、`string` を純粋なプリミティブ値(Primitive Value)として厳格に扱っているからだ。`String` オブジェクトはオブジェクト(Object)であり、プリミティブの枠外にいる。

—

2. 実行時爆弾:typeof と比較演算子の裏切り

型システムでの不整合だけならまだマシだが、最大の悪夢は実行時(Runtime)に訪れる。JavaScriptのラッパーオブジェクトは、型を破壊するバグやセキュリティ脆弱性の温床になる。

① `typeof` の欺瞞

const s1 = “architect”; // プリミティブ
const s2 = new String(“architect”); // ラッパーオブジェクト

console.log(typeof s1); // “string”
console.log(typeof s2); // “object” 💥

`typeof s2` が `”object”` を返すため、型ガードやバリデーションロジック(`typeof val === ‘string’`)をいとも簡単にすり抜ける。APIリクエストのペイロード検証などでこれをやられると、予期せぬオブジェクトがビジネスロジックの深部へ侵食することになる。

② 参照比較(`===`)の破綻

const s1 = “architect”;
const s2 = new String(“architect”);

console.log(s1 === s2);
// ❌ false (プリミティブとオブジェクトの厳密等価性は成立しない)

console.log(s2 == “architect”);
// ⚠️ true (暗黙の型変換が走り、バグの発見を遅らせる)

条件分岐における `===` の不一致は、キャッシュのヒットミス、データベースのユニーク制約のすり抜け、認証トークンの検証エラーなど、致命的な障害を引き起こす。

—

3. V8エンジン内部:メモリ最適化とボクシングのコスト

では、パフォーマンスの観点からはどうだろうか。ここからがシニアエンジニアが知るべき低レイヤの話だ。

JavaScriptエンジン(V8など)は、`string` などのプリミティブ値をCPUキャッシュに優しく、ヒープを圧迫しない効率的な形式でインライン管理している(例:V8のSm1 / 32bitポインタ表現や、文字列のハッシュ化・スライス最適化)。

しかし、`new String(“…”)` を使って大文字のオブジェクトを生成した瞬間、以下のコストが発生する。

1. ヒープアロケーション: ガベージコレクション(GC)の管理対象となるオブジェクトがヒープ上に強制的に生成される。
2. ボクシング(Boxing)のオーバーヘッド: プリミティブ値をラップするための余分なメモリ消費と、プロトタイプチェーンの参照解決コスト。
3. イベントループへの負荷: 不必要なオブジェクト生成は、V8のマイナーGC(Scavenger)の頻度を高め、メインスレッドのイベントループのキュー消費にマイクロ秒単位の遅延(Jank)を蓄積させる。

TypeScriptで `string` と書いた場合、コンパイラはそれをJavaScriptのプリミティブにコンパイルする。しかし、誤って `String` 型を基準にした設計やキャストを行っていると、開発者が意図しないところでオブジェクトの生成・破棄が繰り返されることになる。

—

4. 正しい型戦略:こう書け

TypeScriptの恩恵を極限まで引き出し、ランタイムの安全性とパフォーマンスを担保するための方針は極めてシンプルである。

規程 1: 型注釈には必ず小文字を使う

// ⭕️ 正しい
function sanitize(input: string): string {
return input.trim();
}

// ❌ 絶対に禁止
function sanitize(input: String): String {
return input.trim();
}

規程 2: 型ガードやブランディング(Branded Types)での厳密化

ドメイン駆動設計などで「ただの文字列ではなく、検証済みのUUIDやメールアドレスである」ことを型で保証したい場合も、ラッパーオブジェクトではなくブランド型(Branded Types)を用いる。

// ブランディング技法:実行時のオーバーヘッドゼロで型安全性を極限まで高める
type Email = string & { readonly __brand: unique symbol };

function createEmail(raw: string): Email {
if (!raw.includes(‘@’)) throw new Error(‘Invalid email’);
return raw as Email; // 実行時はただの string のまま!
}

const userEmail = createEmail(“architect@example.com”);
// userEmail は string として扱えるが、Email 型を要求する関数にそのまま渡せる

このアプローチであれば、メモリ上は単なるプリミティブの `string` でありながら、コンパイラレベルで厳密な型制約を課すことができる。

—

結び:型システムは言語の重みを映す鏡である

TypeScriptの `String` と `string` の非対称性は、JavaScriptという動的言語の歴史的経緯と、TypeScriptが目指す静的安全性の妥協点が生んだ「歴史的構造物」に他ならない。

言語の仕様書を斜め読みしただけでは、なぜ小文字を使わなければならないのかの本質にはたどり着けない。V8のメモリモデル、ボクシングのメカニズム、そしてコンパイラの型評価の仕組みまで見通して初めて、堅牢なシステムアーキテクチャの構築が可能となる。

コードベースから大文字のプリミティブ型を駆逐せよ。型は、単なるドキュメントではない。それはランタイムの安全性を担保する最強の防壁なのだから。

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