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

コードレビューをしていて、最も「おっ」と目が留まり、そして静かに修正を促す瞬間がある。
それが、型定義やパラメータの型注釈に `string` ではなく `String`(大文字のS)が使われている場面だ。

「動くからいいじゃないか」と思うかもしれない。しかし、TypeScriptの型システムとJavaScriptのランタイムの裏側を知るテックリードからすれば、これは「意図しないバグの温床」であり、「型システムの破壊」に他ならない。

今回は、なぜプリミティブ型としての `string` を選ぶべきで、ラッパーオブジェクトとしての `String` が魔物であるのか。型推論、コンパイル結果、そして実務での堅牢な設計パターンの観点から、徹底的に解き明かしていこう。

—

1. プリミティブとオブジェクトの非対称性:型システムが視ているもの

まず、TypeScriptのコンパイラが `string` と `String` をどう評価しているのか、その根本的な違いを確認する。

  • `string` (小文字): JavaScriptのプリミティブ型(リテラル値)。メモリ上に値そのものが存在し、軽量でイミュータブル。
  • `String` (大文字): ECMAScriptのラッパーオブジェクト(コンストラクター関数)。`new String(“hello”)` によって生成されるインスタンスであり、型としては「オブジェクト」に分類される。

TypeScriptの型定義(`lib.d.ts`)を覗いたことがあるなら知っているはずだ。実は、`string` 型の変数に対してプロパティ(`.length` や `.toUpperCase()` など)にアクセスできるのは、TypeScriptおよびJavaScriptのエンジンが「オートボクシング(Auto-boxing)」という闇の裏技を裏で発動しているからに過ぎない。一時的にラッパーオブジェクトに包み、処理が終わったら即座に捨てることで、パフォーマンスと利便性を両立させている。

では、これをあえて大文字の `String` で型注釈すると何が起きるか。コードで見てみよう。

// 【アンチパターン】大文字の String を使った型定義
function processUsername(name: String): void {
// 一見、何も問題なさそうに見える
console.log(name.toUpperCase());
}

// 1. リテラル(プリミティブ)を渡す -> コンパイル通る(オートボクシングのおかげ)
processUsername(“yusuke”);

// 2. わざわざオブジェクトとして生成して渡す -> 当然通る
processUsername(new String(“yusuke”));

「あれ、通るじゃん。どこが問題なの?」と思った読者、ここからが本番だ。型システムの厳密性を舐めてはいけない。

—

2. なぜ `String` を使ってはいけないのか?(3つの致命的理由)

実務において `String` 型を使用することがご法度である理由は、主に以下の3点に集約される。

① 型の厳密性が崩壊し、`object` が紛れ込む

`String` はオブジェクト型であるため、型の範囲が広すぎる。例えば、APIレスポンスのバリデーションや、厳密な型ガードをすり抜けて、意図しないオブジェクト構造が入り込む余地を与えてしまう。

② 比較演算子(`===`)の裏切り

これが最大のトラップだ。JavaScriptにおいて、プリミティブとオブジェクトは `===` で比較したとき、絶対に一致しない。

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

console.log(primitiveStr === objectStr);
// 🔴 実行結果: false !!
// 型は「同じようなもの」に見えても、ランタイムでは完全に別物。

もしドメインロジックで「入力値が特定の文字列と一致するか」を判定する際、一方が `String` オブジェクトとして混入していると、テストが緑色にならない不可解なバグに数時間悩まされることになる。

③ JSONシリアライゼーションの罠

API通信(`fetch` や Axios)でペイロードを送信する際、`JSON.stringify()` はプリミティブな文字列を正しくシリアライズするが、`new String(“foo”)` のようなインスタンスを渡すと、なんと `{ “0”: “f”, “1”: “o”, “2”: “o” }` のような配列風のオブジェクトに変換されてしまう。
バックエンドのAPIサーバーは発狂し、400 Bad Requestの嵐となるだろう。

—

3. 実務で遭遇する「安全な設計パターン」とコード例

では、フロントエンド開発やAPI連携において、どのように型を設計し、安全性を担保すべきか。
コンポーネントのプロパティ設計と、非同期APIレスポンスのバリデーションを例に、プロダクションクオリティのコードを見ていこう。

/

  • 堅牢なユーザープロファイル・コンポーネントの型設計
  • 【教訓】
  • – プロパティの型は必ずプリミティブの `string` を使用する。
  • – `String` や `Boolean`、`Number` といったラッパーオブジェクト型は一切排除する。

/

import { z } from ‘zod’; // 実行時バリデーションのデファクト

// 1. ドメインモデルの定義(プリミティブで統一)
export interface UserProfile {
readonly id: string;
readonly username: string;
readonly email: string;
readonly bio?: string; // オプショナルも string
}

// 2. APIレスポンスの実行時検証スキーマ
// 外部からのデータは信用できないため、Zod等で string であることを強制する
export const UserProfileSchema = z.object({
id: z.string(),
username: z.string().min(3, “ユーザー名は3文字以上である必要があります”),
email: z.string().email(“不正なメールアドレス形式です”),
bio: z.string().optional(),
});

// 3. コンポーネントのプロパティ定義
export type UserProfileCardProps = {
readonly profile: UserProfile;
readonly onUpdateBio: (newBio: string) => Promise; // ここも当然 string
};

/

  • プロダクション品質のコンポーネント実装イメージ

/
export const validateAndTransformResponse = (rawData: unknown): UserProfile => {
// パース時に String(大文字) オブジェクトが紛れていても、
// Zodの z.string() はプリミティブの string に綺麗に正規化(または弾く)してくれる
const result = UserProfileSchema.safeParse(rawData);

if (!result.success) {
throw new Error(`Invalid API Response: ${result.error.message}`);
}

return result.output;
};

このコードにおいて、妥協は一切ない。すべての型注釈は小文字の `string` で統一されており、外部からどんなデータが飛来しようとも、型システムと実行時バリデーションの二重の防壁によって守られている。

—

4. チーフアーキテクトからの提言:Linterで「未来のバグ」を物理的に防ぐ

人間の注意力に頼ったコードレビューはスケールしない。チーム開発において `String`(大文字)や `Boolean`、`Number` のようなラッパーオブジェクト型の使用を根絶するには、静的解析ツールの力学を利用するのがエンジニアリングとしての正しいアプローチだ。

ESLintを使用しているなら、`@typescript-eslint/ban-types` ルール(あるいは後継の strict な設定)を有効にし、次のように設定しておこう。

{
“rules”: {
“@typescript-eslint/ban-types”: [
“error”,
{
“types”: {
“String”: {
“message”: “プリミティブ型の ‘string’ を使用してください。大文字の ‘String’ はオブジェクトを生成するためバグの原因になります。”,
“fixWith”: “string”
},
“Boolean”: {
“message”: “プリミティブ型の ‘boolean’ を使用してください。”,
“fixWith”: “boolean”
},
“Number”: {
“message”: “プリミティブ型の ‘number’ を使用してください。”,
“fixWith”: “number”
}
}
}
]
}
}

このルールをCI/CDパイプラインに組み込むだけで、うっかり `String` を書いたコードはビルドすら通らなくなる。これぞ、システムで担保する堅牢なアーキテクチャだ。

—

まとめ

  • `string` はプリミティブ、`String` はオブジェクト。 この非対称性を理解せよ。
  • `String` オブジェクト型を使うと、比較演算子の破綻、JSONシリアライズのバグ、意図しない型範囲の拡大を招く。
  • フロントエンドのコンポーネント設計、API連携のいずれにおいても、型は常にプリミティブ(`string`, `number`, `boolean`)で統一する。
  • 規約やレビューだけでなく、Linterを用いて機械的にミスを排除する仕組みを構築せよ。

TypeScriptの型システムは、正しく使えばあなたのコードベースを鉄壁の要塞に変えてくれる。その第一歩は、基本中の基本である「小文字と大文字の選択」という極めてミクロな視点へのこだわりにあるのだ。

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