こんにちは!TypeScriptの世界へようこそ。
日々フロントエンドからNode.jsまで、コードを書くのが楽しくて仕方がない先輩エンジニアです。
今回は、TypeScriptの型システムの中でも、特に初学者の多くが「えっ、どういうこと!?」と頭を抱えがちな、「プリミティブ型とインターセクション(`&`)の奇妙な関係」についてお話しします。
ここをクリアすれば、TypeScriptの型システムが「ただの制限」ではなく、「美しく厳密な論理の世界」であることに気づけるはずです。しっかり一緒にマスターしていきましょう!
—
1. インターセクション型(`&`)の基本と、思い込みの罠
まずは、インターセクション型(交差型)の基本的な使い方をおさらいしておきますね。
`&` は、複数の型を「合体」させて、すべての条件を満たす新しい型を作るためのものです。
オブジェクト型に対して使うときは、直感的にわかりやすい動きをします。
// 名前の型
type HasName = {
name: string;
};
// 年齢の型
type HasAge = {
age: number;
};
// 2つをインターセクションで結合
type User = HasName & HasAge;
// 使用例:両方のプロパティを必ず持っている必要があります
const teacher: User = {
id: 1, // ❌ エラー! id は定義されていません
name: “Taro”,
age: 30,
};
オブジェクトの結合であれば、「あれも持ってて、これも持っている完全版のオブジェクト!」という感覚でスムーズに理解できますよね。
さて、ここからが本題です。
では、この `&` を 「プリミティブ型(string, number, booleanなど)」 に対して使ったらどうなるでしょうか?
「文字と数字を合体させるから……文字列と数値を両方兼ね備えたスーパーストリング的な何かになるのかな?」
そう思ってコードを書くと、TypeScriptのコンパイラから冷徹な現実を突きつけられます。
—
2. プリミティブを `&` で結ぶと、なぜ `never` になるのか?
実際に試してみましょう。
type StrAndNum = string & number;
// 結論:この型は 「never」 になります
……えっ、`never`? なんだそれは?と思いましたよね。
`never` 型とは、一言で言えば「絶対に値が存在しえない(空集合)」ことを表す型です。ここに代入できる値は、TypeScriptの世界には文字通り「ひとつも存在しません」。
なぜ `string & number` が `never` になってしまうのでしょうか?
ここに、TypeScriptの型システムの根幹にある「集合論(数学)」の考え方があります。
脳内イメージ:円が重なる場所はどこ?
型を「値の集合(集まり)」として考えてみてください。
- `string` 型という円の中には、`”hello”`, `”TypeScript”`, `””` といった「すべての文字列」が無数に入っています。
- `number` 型という円の中には、`1`, `42`, `3.14`, `-5` といった「すべての数値」が無数に入っています。
ここで、インターセクション(`&`)は、数学でいう「積集合(AND:両方に属する共通部分)」を意味します。
[ string 型の円 ] [ number 型の円 ]
“hello” 1
“TypeScript” 42
\ /
\ 重なる場所は? /
—> 【 空っぽ 】 <---
「文字であり、かつ数値でもある」ような値は、この現実世界にも、プログラミングの世界にも存在しませんよね。「1つの値が、同時に完全に `string` であり、かつ `number` である」ことは不可能です。
そのため、両方の条件を同時に満たす値の範囲は「存在しない」となり、結果として `never` という評価を下されるのです。
—
3. リテラル型で考えると、さらにハッキリわかる
もう少し具体的に、特定の文字(リテラル型)同士で試してみると、この仕組みがさらに腑に落ちます。
type A = “apple” & “apple”;
// これは? -> “apple” (同じもの同士の交わりなので成立する)
type B = “apple” & “banana”;
// これは? -> never (「アップル」であり、かつ「バナナ」であるものは存在しない!)
`”apple” & “banana”` が `never` になるのは当然ですよね。りんごであると同時にバナナである果物は、この世に存在しません。
プリミティブ型同士の結合(`string & number` など)も、これと全く同じ現象が起きているだけなんです。
—
4. 陥りやすい文法エラーと、実務での注意点
初学者のうちや、複雑なジェネリクス(型の引数)を自作し始めると、意図せずこの「プリミティブのインターセクション」を踏んでしまい、バグに悩まされることがあります。
よくある落とし穴:ジェネリクスでの誤った制約
例えば、何でも受け取れる関数を作ろうとして、以下のようなコードを書いたとします。
// ⚠️ やりがちな例
function processValue
// …
}
もし呼び出し側がこの関数に `number` 型の値を渡そうとすると、内部で `T` が `number` に推論され、引数の型は `number & string`(すなわち `never`)になってしまいます。その結果、「何も値を渡せない(正確には `never` しか渡せない)バグった関数」が完成してしまいます。
processValue(123);
// 引数に何を入れてもコンパイルエラーになるか、型が崩壊します!
どう対処すべきか?
異なる型の可能性を許容したい場合は、インターセクション(`&`:かつ)ではなく、ユニオン型(`|`:または)を使いましょう。
// 正しいアプローチ:「文字列、または数値」を受け入れる
type StrOrNum = string | number; // これならOK!
function processValue(value: string | number) {
// 型ガードなどで分岐して安全に処理できる
if (typeof value === “string”) {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}
「かつ(`&`)」にするべきか、「または(`|`)」にするべきか。ここを混同しないことが、TypeScriptを使いこなす第一歩です。
—
まとめ:ここをクリアすればTypeScriptの基本はバッチリ!
今回は、インターセクション型とプリミティブ型の奇妙な関係について解説しました。
- インターセクション(`&`)は「両方の条件を満たす(積集合)」という意味。
- `string & number` のように、同時に満たせないプリミティブを結合すると、矛盾が生じて `never`(存在しない型)になる。
- 複数の可能性を許容したいときは、`&` ではなくユニオン型(`|`)を使う。
TypeScriptの型システムは、一見すると厳しく面倒くさく思えるかもしれませんが、その裏側には非常に一貫した「数学的・論理的な整合性」があります。コンパイラは、私たちが矛盾したコードを書かないように、数学の厳密さをもって守ってくれているんですね。
ここを理解できたあなたなら、もう型エラーを恐れる必要はありません。
明日からのコーディングが、より楽しく、よりロジカルなものになるはずです。一緒にTypeScriptを極めていきましょう!