【実務・中級編】 プリミティブ型とラッパーオブジェクト型 – TypeScript実践ガイド

TypeScriptの「ラッパーオブジェクト」という罠:なぜ `String` を型定義に使ってはいけないのか

現場でコードレビューをしていると、稀に遭遇する光景がある。「型定義がなんとなく動くから」という理由で、プリミティブ型ではなくラッパーオブジェクト型(`String`, `Number`, `Boolean`)を型注釈に使っているコードだ。

「え、これ動くし別にいいんじゃないの?」

そう思った君へ。これは単なる好みの問題ではない。TypeScriptの型システムと、JavaScriptの実行エンジンであるブラウザが裏側で何をしているかを知れば、それがどれほど危険で無意味な「地雷」であるかが見えてくるはずだ。

今日は、なぜプリミティブ型(`string`, `number`)一択なのか、その深淵を覗いてみよう。

—

1. プリミティブ型 vs ラッパーオブジェクト型:その決定的な違い

まず、基本をおさらいしよう。TypeScriptで型を定義する際、以下の2つを混同してはならない。

  • プリミティブ型: `string`, `number`, `boolean`
  • これらは「値そのもの」を指す。軽量で、メモリ効率も最高だ。
  • ラッパーオブジェクト型: `String`, `Number`, `Boolean`
  • これらは`new String(“hello”)`のようにインスタンス化可能なコンストラクタ関数を指す。

TypeScriptの型システムにおいて、`String`型は「`string`プリミティブ」と「`String`オブジェクト」の両方を受け入れてしまう。一見便利そうだが、これが型安全性を崩壊させる入り口になる。

2. なぜ `String` 型を使ってはいけないのか

最大の理由は、「意図しない型が混入してもコンパイルが通ってしまう」という脆弱性だ。

// 悪い例:ラッパーオブジェクト型を使うと何が起きるか
function greet(name: String) {
console.log(`Hello, ${name}`);
}

// プリミティブは通る
greet(“Alice”);

// インスタンスも通ってしまう
greet(new String(“Bob”));

// 最悪なケース:なんと、これすら型チェックをすり抜ける
// string以外のオブジェクトが紛れ込んでも、型システムが止めてくれない
greet({} as any);

`String`型は、実は`Object`型のサブタイプに近い挙動をする。プリミティブな`string`しか受け付けたくない関数に、`new String()`が渡されると、期待する動作が狂うだけでなく、デバッグが困難なバグの温床となる。

3. ブラウザの裏側で起きていること

JavaScriptエンジン(V8など)は、開発者が`”hello”.length`と書いた瞬間に、裏でこっそり一時的なラッパーオブジェクトを生成している。これを「オートボクシング」と呼ぶ。

1. `”hello”`(プリミティブ)に`.length`を呼び出す。
2. エンジンが「おっと、プリミティブにメソッドはないな」と判断。
3. 一時的に `new String(“hello”)` を生成する。
4. メソッドを実行し、結果を返したらそのオブジェクトを即座に破棄する。

つまり、ラッパーオブジェクトはブラウザが内部的に使う「一時的な道具」であって、僕たちが型定義で明示的に使うべきものではないんだ。自分で`new String()`を使うコードを書くことは、モダンなJavaScript開発においてまずあり得ない。

4. 実務で使える「型定義の黄金律」

現場で迷ったら、以下のルールを脳に刻んでおいてほしい。

> 「型定義には必ずプリミティブ型(小文字)を使う」

ベストプラクティス:比較コード

// ✅ 正解:プリミティブ型を使う
interface User {
id: number;
name: string;
isActive: boolean;
}

// ❌ 不正解:ラッパーオブジェクト型は使わない
interface BadUser {
id: Number; // Number型は使わない!
name: String; // String型は使わない!
isActive: Boolean;// Boolean型は使わない!
}

// なぜダメか:
const user: BadUser = {
id: new Number(1), // インスタンス化して代入できてしまう
name: “Taro”,
isActive: true
};

// 計算で痛い目を見る例
// Numberオブジェクト同士の比較は厳密には等価にならないことがある
console.log(new Number(10) === new Number(10)); // false! (参照の比較になるため)

結び:シニアからのアドバイス

TypeScriptは「静的な型安全」を担保するためにある。ラッパーオブジェクト型を許容するということは、その安全性をわざわざドブに捨てているのと同じだ。

「動くコード」を書くのはジュニアでもできる。しかし、「絶対に予期せぬ型が混入しない堅牢なコード」を書くのが、僕たちフロントエンド・スペシャリストの仕事だ。

もし既存のプロジェクトで`String`や`Number`が使われていたら、迷わず`string`や`number`に書き換えてほしい。その小さな積み重ねが、半年後の君自身のデバッグ時間を劇的に減らすはずだ。

次は、`any`と`unknown`の境界線について深く掘り下げてみようか。あれもまた、現場のコードを壊す「魔物」だからね。

コメント

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