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`の境界線について深く掘り下げてみようか。あれもまた、現場のコードを壊す「魔物」だからね。

コメント