プリミティブ型における「小文字」と「大文字」の非対称性:なぜ `String` を使ってはいけないのか
TypeScriptのコードベースを監査していると、いまだに散見されるアンチパターンがある。それが、型注釈や関数のシグネチャにおける `String`、`Number`、`Boolean` といった「ボクシングされたラッパーオブジェクト型」の使用だ。
// ⚠️ 現場のコードで絶対に避けるべきアンチパターン
function processIdentifier(id: String): void {
// …
}
「動くからいいのではないか」「厳密に文字列を受け取っているのだから問題ない」と思ったのなら、TypeScriptの型システムとV8エンジン(あるいはJavaScriptランタイム)の裏側で何が起きているのかを根本から見誤っている。
本稿では、プリミティブの小文字(`string`)と大文字(`String`)の間に存在する決定的な非対称性を、コンパイラの型評価、メモリレイアウト、そしてV8の隠れクラス(Hidden Classes)とインラインキャッシュ(ICs)の最適化メカニズムの観点から剥ぎ取っていく。
—
1. 型システム層:`string` は「値」であり、`String` は「プロトタイプチェーンの親」である
まず、TypeScriptの型定義ファイル(`lib.d.ts`)の内部構造を覗いてみよう。ここには、言語仕様の設計思想が赤裸々に刻まれている。
// lib.es5.d.ts の簡略化された表現
type string = / プリミティブな文字列リテラル・値の型 /;
interface String {
readonly 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`(プリミティブ型)
`string` は、JavaScriptのプリミティブ値そのものを表す型である。メモリ上に純粋なUTF-16(またはV8の内部表現)の文字シーケンスとして存在し、余分なメタデータを持たない。
大文字の `String`(オブジェクト型)
一方、`String` はインターフェース(またはクラスのコンストラクター関数)として定義されている。これは `new String(“hello”)` によって生成されるラッパーオブジェクトの構造を規定する型だ。
この2つを代入互換性の観点から評価してみる。
const primitiveStr: string = “architect”;
const boxedStr: String = new String(“architect”);
// 1. プリミティブは、String型(オブジェクト型)の変数に代入できるか?
const a: String = primitiveStr;
// ✅ 通過する。なぜなら、TypeScriptはプリミティブがメソッド呼び出し時に
// 自動ボクシング(Autoboxing)されるため、構造的サブタイピングの観点から
// Stringのプロパティをすべて満たしているとみなすからだ。
// 2. では、String型(オブジェクト型)は、プリミティブ型に代入できるか?
const b: string = boxedStr;
// ❌ コンパイルエラー!
// Type ‘String’ is not assignable to type ‘string’.
// Type ‘String’ has no properties in common with type ‘string’.
この非対称性こそが、最初のトラップである。`String` 型の変数を `string` を期待する関数に渡そうとした瞬間、型システムは容赦なくコンパイルエラーを吐き出す。なぜなら、オブジェクトはプリミティブではないからだ。
—
2. ランタイム層:V8エンジンを殺す「ボクシング」とメモリの無駄遣い
型安全性の話だけであれば「大文字のほうが制約が厳しいだけなら、厳格に書けばいいのでは?」と勘違いするかもしれない。しかし、実行時(Runtime)のパフォーマンスとメモリ効率において、`String` の使用は百害あって一利なしである。
ヒープアロケーションの呪縛
JavaScriptのプリミティブ値(`string`, `number`, `boolean`)は、変数のスコープや状況に応じて、スタック上、あるいはV8の若い世代のヒープ(New Space)にインラインで効率よく配置される。
しかし、`new String(“foo”)` を実行した瞬間、あるいは `String` 型のインスタンスを生成した瞬間、以下のコストが発生する。
1. ヒープ上へのオブジェクト生成: ガベージコレクション(GC)の管理対象となる実体オブジェクトがメモリ上に確保される。
2. ポインタのデリファレンス: 単なる値の読み出しではなく、メモリアドレスを辿る(ポインタ参照)オーバーヘッドが生じる。
3. 隠れクラス(Hidden Classes / Shapes)の生成: V8はオブジェクトのプロパティ構造を最適化するためにHidden Classを付与するが、不要なラッパーオブジェクトの乱立はメモリ空間を汚染する。
以下のベンチマーク的コードを見れば、その差は歴然だ。
// ── 実行時の挙動の差 ──
const pStr: string = “perf_test”;
const oStr: String = new String(“perf_test”);
console.log(typeof pStr); // “string” (プリミティブ)
console.log(typeof oStr); // “object” (オブジェクト!)
console.log(pStr === oStr); // false (型も実体も異なる)
`pStr === oStr` が `false` になるのは当然として、実務上もっとも恐ろしいのは、「文字列比較のバグ」や「JSONシリアライゼーションの崩壊」だ。
const payload = {
id: new String(“999”)
};
// これをJSON.stringifyするとどうなるか?
console.log(JSON.stringify(payload));
// 出力: {“id”:{}}
// 🚨 なんと、空のオブジェクトになってしまう!
`String` オブジェクトのプロパティは列挙不可(non-enumerable)であるため、`JSON.stringify` やスプレッド構文 (`{ …payload }`) を通したときに、中身が綺麗に消え去るか、意図しない構造破壊を引き起こす。これはセキュリティ上の脆弱性や、データ損失バグの温床となる。
—
3. コンパイラAPI・抽象構文木(AST)レベルでの静的解析の罠
私たちアーキテクトがカスタムESLintルールやTypeScriptの Compiler API を用いてコードベースを解析する際、大文字の `String` はしばしば厄介な偽陽性(False Positive)を引き起こす。
例えば、型ガードやオーバーロードの設計において、`String` を許容してしまうと、関数の呼び出し側で以下のような歪んだコードが正当化されてしまう。
function sanitize(input: string | number): string {
if (typeof input === “string”) {
// ここでの input はプリミティブの string
return input.trim();
}
return String(input); // これは「関数(キャスト)」としての String() であり、new ではないためセーフ
}
注意すべきは、グローバル関数としての `String(val)`(小文字で始まるが関数として呼び出すもの、型定義上は `StringConstructor`)は、プリミティブの `string` を返すという点だ。
const result = String(123); // 型は “string”(プリミティブ)になる
しかし、TypeScriptの型注釈として `x: String` と書いた場合は、関数ではなくオブジェクト型としての `String` インターフェースを指してしまう。この「関数としての `String()`」と「型としての `String`」の二面性が、初学者だけでなく中堅エンジニアをも混乱させる最大の要因である。
—
4. チーフアーキテクトからの提言:型定義の厳格化と静的防御
この非対称性を完全にハックし、チーム全体のコードベースをマクロな視点から保護するためには、ESLintを用いた機械的な強制が不可欠だ。
手動のコードレビューに頼るようでは、アーキテクトとしての怠慢である。`@typescript-eslint` を用いて、大文字のプリミティブ型ラッパーの侵入をコンパイル・リント段階で完全にブロックせよ。
推奨する ESLint 設定 (`eslint.config.js`)
export default [
{
rules: {
// プリミティブのラッパーオブジェクト型の使用を厳禁とする
“@typescript-eslint/no-wrapper-object-types”: “error”,
// 旧:@typescript-eslint/ban-types で設定されていた主要なターゲットを完全に封鎖
// String, Number, Boolean, Symbol, Object の大文字使用を一切許容しない
}
}
];
さらに、TypeScriptの `tsconfig.json` においても、厳格な型チェックフラグをすべて有効化しておくことが大前提となる。
{
“compilerOptions”: {
“strict”: true,
“noImplicitAny”: true,
“strictNullChecks”: true,
“target”: “ESNext”,
“module”: “NodeNext”
}
}
—
結び
TypeScriptにおける `string` と `String` の非対称性は、JavaScriptの歴史的経緯(プロトタイプベースのオブジェクト指向と、後から持ち込まれたプリミティブ型の同居)が型システムにそのまま投影された結果生じた、言わば「必要悪の遺物」である。
言語の仕様がそれを許しているからといって、実務の最前線でそれを書く理由にはならない。
- `string` は、メモリに優しく、シリアライズ可能で、予測可能なプリミティブ値である。
- `String` は、無駄なヒープ領域を消費し、JSON化で爆発し、意図せぬ比較バグを生むレガシーなラッパーオブジェクトである。
型システムの本質を理解したエンジニアであれば、迷うことなく小文字のプリミティブを選ぶはずだ。コードの背後にあるランタイムの挙動まで見通す視点を持ち、妥協のない型設計を貫いてほしい。