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

コードレビューの最中、ジュニアやミドルクラスの開発者が書いたコンポーネントの型定義を見て、思わず手を止めてため息をついたことはないだろうか。

// ❌ やってはいけないアンチパターン
interface UserProps {
name: String;
id: Number;
isActive: Boolean;
}

「動くからいいじゃないか」と思うかもしれない。しかし、TypeScriptの型システムとJavaScriptのランタイムの現実を知る者にとって、この大文字の `String`、`Number`、`Boolean`(以下、総称してボックス化ラッパー型と呼ぶ)の乱用は、コンパイル時の型安全性に対する「静かなるテロ」に等しい。

今回は、なぜTypeScriptのプリミティブ型において「小文字」と「大文字」がこれほどまでに非対称なのか、そしてそれがコンパイラとランタイムに何をもたらすのかを、チーフアーキテクトの視点から徹底的に解剖しよう。

—

1. プリミティブ(`string`)とラッパー(`String`)の決定的な断絶

まず、TypeScriptの型システムがどのように構築されているかを直視してほしい。

TypeScriptにおける `string`(小文字)はプリミティブ型であり、値そのものを表す。一方、`String`(大文字)はJavaScriptのグローバルコンストラクタであり、TypeScriptの型定義においては「そのコンストラクタから生成されるインスタンス(およびプロトタイプチェーンを共有するオブジェクト)」を指す。

const a: string = “hello”; // プリミティブ
const b: String = “hello”; // ラッパーオブジェクト(およびそのプリミティブ)

// ここまではTypeScriptの型システムが自動ボックス化を許容するため、一見通る
const c: String = new String(“hello”); // これは完全に「オブジェクト」

では、次のコードはどう評価されるだろうか?

function printLength(s: String) {
console.log(s.length);
}

printLength(“hello”); // 通過する
printLength(new String(“hello”)); // これも通過する

「ほら、問題ないじゃないか」と思ったあなた。TypeScriptをまだ半分しか理解していない。問題は、この大文字の `String` 型が、不必要に広範な値を許容し、型ガードやユニオン型の絞り込み(Narrowing)を完全に破壊する点にある。

なぜ `String` を使うべきではないのか?

1. 型幅(Type Universe)が広すぎる
`String` 型は、`string` プリミティブだけでなく、`String` オブジェクトのインスタンスも内包する。さらに言うと、`String` 型はJavaScriptのプロトタイプチェーン上のメソッド(`valueOf`, `toString`, `charAt` など)を持つオブジェクト全般を指すようになってしまい、予期せぬオブジェクトが混入する隙を与える。
2. `typeof` 演算子との致命的な乖離
ランタイムにおいて、`typeof “hello”` は `”string”` を返す。しかし、`typeof new String(“hello”)` は `”object”` を返す。この言語仕様の闇を、大文字の `String` 型はそのまま型システムに持ち込んでしまう。
3. 等価性(Equality)のバグの温床

const strPrimitive: string = “foo”;
const strObject: String = new String(“foo”);

console.log(strPrimitive === strObject); // ❌ false になる!

実務のAPIレスポンスの比較や、Reactのコンポーネントのprops比較において、このミスは二度とデバッグしたくないような不可解なバグを引き起こす。

—

2. 実務で遭遇する「最悪のシナリオ」と堅牢な設計パターン

非同期APIから受け取るユーザーデータを処理するフロントエンドのコードを考えてみよう。ここでラッパーオブジェクト型を使ってしまうと、TypeScriptの恩恵である「網羅性チェック(Exhaustiveness Checking)」が機能しなくなる。

以下の、「バグを完全に排除し、保守性を極限まで高めたプロダクションコード例」を見てほしい。

/

  • —————————————————————-
  • 堅牢なユーザープロフィール管理モジュール
  • —————————————————————-
  • チーフアーキテクトの知見:
  • すべての型定義で小文字のプリミティブ(string, number, boolean)を使用し、
  • 不正なオブジェクトの混入をコンパイル段階で完全にシャットアウトする。

/

// 1. ドメインモデルの定義(必ずプリミティブを使う)
export type UserId = string;
export type Email = string;

export interface UserProfile {
readonly id: UserId;
readonly username: string;
readonly email: Email;
readonly loginCount: number;
readonly isVerified: boolean;
}

// 2. APIレスポンスの型(外部からの入力を想定)
// 悪意のある、あるいはレガシーなAPIがラッパーオブジェクトを返してきた場合を弾くための型
type StrictPrimitive = T;

export interface RawUserApiResponse {
readonly id: StrictPrimitive;
readonly username: StrictPrimitive;
readonly email: StrictPrimitive;
readonly loginCount: StrictPrimitive;
readonly isVerified: StrictPrimitive;
}

/

  • 3. バリデーション&マッパー関数
  • ランタイムにおける型のゆがみをここで確実に検知し、安全なドメインモデルに変換する。

/
export function sanitizeUserProfile(raw: unknown): UserProfile {
if (typeof raw !== “object” || raw === null) {
throw new TypeError(“Invalid payload: expected an object.”);
}

const { id, username, email, loginCount, isVerified } = raw as Record;

// ここでプリミティブであることを厳格に担保する
// ※ typeof new String(“x”) は “object” なので、このガードで確実に弾かれる!
if (typeof id !== “string” || typeof username !== “string” || typeof email !== “string”) {
throw new TypeError(“Type mismatch: ID, username, and email must be primitive strings.”);
}

if (typeof loginCount !== “number” || Number.isNaN(loginCount)) {
throw new TypeError(“Type mismatch: loginCount must be a valid primitive number.”);
}

if (typeof isVerified !== “boolean”) {
throw new TypeError(“Type mismatch: isVerified must be a primitive boolean.”);
}

return {
id,
username,
email,
loginCount,
isVerified,
};
}

// — 使用例 —
const apiData: unknown = {
id: “usr_992183”,
username: “architect_K”,
email: “core.dev@typescript.example”,
loginCount: 42,
isVerified: true,
};

const safeProfile = sanitizeUserProfile(apiData);
console.log(`Successfully parsed user: ${safeProfile.username}`);

このコードの美しさは、「コンパイル時の型システム(TypeScript)」と「実行時の安全性(JavaScriptの `typeof` ガード)」が完全に同期している点にある。もしプロパティに `String` オブジェクトが混入していた場合、`sanitizeUserProfile` 内のランタイムチェックが即座にそれを検知し、アプリのクラッシュを未然に防ぐ。

—

3. コンパイラ視点:なぜ小文字を使うべきなのか(パフォーマンスと型推論)

TypeScriptコンパイラ(`tsc`)の内部構造においても、大文字のラッパー型と小文字のプリミティブ型は全く異なる扱é方をしている。

コンパイラは、小文字の `string` を見ると、それを内部の特殊なプリミティブ型シンボルとして即座にメモリ上にキャッシュし、部分型関係(Subtyping)の判定を高速に行う。
一方で、`String` のような大文字の型は、通常の「インターフェース型(Interface Type)」や「クラスインスタンス型」として処理される。つまり、型チェックのたびにプロトタイプチェーンの探索やメソッドのオーバーロードの解決といった余計な計算コストが発生する。

わずか数ミリ秒の話に聞こえるかもしれないが、数万行規模の大規模モノレポフロントエンドにおいて、このような無駄な型解決が積み重なることの弊害は計り知れない。Language Server(IDEの補完機能)のレスポンスが重くなる原因の多くは、こうした不適切なラッパー型の多用にある。

—

結論:コードレビューのチェックリストとして

明日から、チームのコードレビューで大文字のプリミティブ型を見かけたら、以下の言葉とともに差し戻してほしい。

> 「その `String` や `Number` は、ランタイムでバグを生み、コンパイルを遅くするだけの遺物だ。今すぐ小文字の `string` と `number` に書き換え、ドメインモデルの境界を堅牢に定義し直してくれ」

TypeScriptの型システムは、JavaScriptの混沌に秩序をもたらすために存在する。その言語の重みとコンパイラの挙動を理解した者だけが、真に保守性の高い、美しいプロダクションコードを書くことができるのだ。

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