第9章:ユニオン型と型ガード

【TypeScript入門シリーズ】第9章:ユニオン型と型ガード

プログラムを書いているとき、関数や変数が複数の型を扱うことが求められる場面があります。TypeScriptでは、ユニオン型と呼ばれる機能を使って、変数や引数に複数の型を許容することができます。また、ユニオン型を使うときには、型ガードを使って安全に型を判別し、プログラムの実行時に型エラーが発生しないようにすることが重要です。

この章では、ユニオン型の基本から型ガードによる型の絞り込みの方法まで、詳細に説明していきます。


8.1 ユニオン型とは?

ユニオン型(Union Types)は、複数の型を持つことができる特殊な型です。例えば、ある変数がstring型またはnumber型のどちらかである可能性がある場合、ユニオン型を使ってその変数を定義できます。ユニオン型は、|(パイプ)記号を使って表現します。

8.1.1 ユニオン型の定義

ユニオン型を使って変数を定義する際、複数の型を|で区切ります。以下の例では、idという変数がstring型またはnumber型である可能性を示しています。

let id: string | number;
id = "ABC123"; // OK: string型
id = 12345; // OK: number型

この例では、id変数がstring型でもnumber型でも良いと定義されています。この柔軟性により、ユニオン型は異なる型のデータを扱う必要がある場面で非常に便利です。

8.1.2 関数のユニオン型引数

ユニオン型は関数の引数にも適用できます。次の例では、printIdという関数がstring型またはnumber型のidを受け取ることができます。

function printId(id: string | number): void {
console.log(`ID: ${id}`);
}

printId(101); // "ID: 101"
printId("XYZ789"); // "ID: XYZ789"

この例のように、ユニオン型を使うことで、同じ関数が異なる型に対応する柔軟な設計が可能になります。


8.2 型ガードとは?

ユニオン型は複数の型を扱うことができる便利な機能ですが、プログラムの中で特定の型に対して異なる処理を行う場合には、型ガード(Type Guards)を使って型を判別する必要があります。型ガードは、変数や引数が特定の型であるかを確認し、その型に基づいて適切な処理を実行するための手法です。

8.2.1 型ガードの基本

型ガードの基本的な方法は、typeof演算子を使って型を判別することです。typeofは、変数の型を判定するための標準的なJavaScriptの機能で、TypeScriptでも使用可能です。

function printId(id: string | number): void {
if (typeof id === "string") {
// idがstring型の場合
console.log(`String ID: ${id.toUpperCase()}`);
} else {
// idがnumber型の場合
console.log(`Number ID: ${id.toFixed(2)}`);
}
}

printId(101); // "Number ID: 101.00"
printId("XYZ789"); // "String ID: XYZ789"

この例では、typeofを使ってidの型を判別し、文字列の場合と数値の場合で異なる処理を実行しています。typeofによってTypeScriptは型が絞り込まれ、id.toUpperCase()id.toFixed(2)のようなメソッドを安全に呼び出せるようになります。


8.3 型の絞り込み(Narrowing)

型の絞り込み(Narrowing)とは、ユニオン型で定義された変数の型を特定の型に限定していくプロセスを指します。型ガードを使って特定の型に絞り込むことで、TypeScriptはその型に固有のメソッドやプロパティにアクセスできるようになります。

8.3.1 typeofによる型の絞り込み

前述のように、typeofは基本的な型絞り込みの手段です。特に、stringnumberbooleansymbolといった基本的な型に対して有効です。

function isString(value: string | number): boolean {
return typeof value === "string";
}

console.log(isString("hello")); // true
console.log(isString(123)); // false

この例では、typeofを使って引数valuestring型かどうかを判定しています。

8.3.2 instanceofによる型の絞り込み

クラスやオブジェクトに対して型の絞り込みを行う際には、instanceof演算子を使います。instanceofは、オブジェクトが特定のクラスのインスタンスであるかどうかを判定するために使用されます。

class Dog {
bark() {
console.log("Woof!");
}
}

class Cat {
meow() {
console.log("Meow!");
}
}

function makeSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}

let dog = new Dog();
let cat = new Cat();
makeSound(dog); // "Woof!"
makeSound(cat); // "Meow!"

この例では、instanceofを使ってDogまたはCatのインスタンスであるかどうかを判別し、それに応じて異なるメソッドを呼び出しています。


8.4 リテラル型とリテラルの絞り込み

TypeScriptでは、リテラル型を使って特定の値を型として扱うことができます。リテラル型とは、"hello"42などの特定の値そのものを型として扱う機能です。ユニオン型と組み合わせて使うと、特定の値に基づいた型の絞り込みが可能になります。

8.4.1 リテラル型の基本

リテラル型は、固定された特定の値を型として指定します。次の例では、"success""error"といった文字列リテラル型を使って、特定の結果を表現しています。

type Status = "success" | "error";

function printStatus(status: Status): void {
if (status === "success") {
console.log("Operation was successful!");
} else {
console.log("Operation failed.");
}
}

printStatus("success"); // "Operation was successful!"
printStatus("error"); // "Operation failed."

この例では、Statusというユニオン型に2つのリテラル型"success""error"が定義されています。関数内でこれらのリテラル型に基づいて型を絞り込むことで、安全に処理を実行できます。

8.4.2 リテラル型の絞り込み

リテラル型の絞り込みは、単にリテラルの値を直接比較することで行えます。TypeScriptは条件によってリテラル型が絞り込まれるため、そのリテラルに基づいた安全な処理を行うことが可能です。

type Direction = "up" | "down" | "left" | "right";

function move(direction: Direction): void {
switch (direction) {
case "up":
console.log("Moving up");
break;
case "down":
console.log("Moving down");
break;
case "left":
console.log("Moving left");
break;
case "right":
console.log("Moving right");
break;
}
}

move("up"); // "Moving up"
move("left"); // "Moving left"

この例では、Directionというリテラル型が定義されており、4つの固定された値しか取ることができません。switch文を使ってリテラルの値を比較し、適切な処理を行っています。


8.5 in演算子を使った型ガード

in演算子は、オブジェクトが特定のプロパティを持っているかどうかを確認するために使用されます。オブジェクトの構造に基づいて型を絞り込むために、inを使った型ガードが有効です。

interface Fish {
swim(): void;
}

interface Bird {
fly(): void;
}

function moveAnimal(animal: Fish | Bird): void {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}

let fish: Fish = { swim: () => console.log("Swimming...") };
let bird: Bird = { fly: () => console.log("Flying...") };

moveAnimal(fish); // "Swimming..."
moveAnimal(bird); // "Flying..."

この例では、in演算子を使ってanimalオブジェクトがswimプロパティを持っているかどうかを確認し、その結果に基づいて型を絞り込んでいます。これにより、FishBirdのどちらかを安全に判別することができます。


8.6 never型と型の絞り込み

TypeScriptのneverは、「絶対に起こり得ない型」を表します。通常、never型はあり得ない状況に遭遇したときや、型の絞り込みが完全に行われたときに現れます。never型を使うことで、コードの網羅性チェックを行うことができます。

8.6.1 never型の例

never型は、通常のプログラムの中ではほとんど直接使うことはありませんが、型安全性を確保するためのエラーチェックや、網羅性を確認する場合に役立ちます。

type Shape = "circle" | "square";

function getArea(shape: Shape): number {
switch (shape) {
case "circle":
return 3.14 * 5 * 5; // 半径5の円の面積
case "square":
return 5 * 5; // 一辺5の正方形の面積
default:
// 型絞り込みが漏れている場合、ここでエラーを出す
const exhaustiveCheck: never = shape;
throw new Error(`Unexpected shape: ${exhaustiveCheck}`);
}
}

この例では、Shape型が"circle""square"しか取り得ないことを前提にしています。defaultケースでnever型を使用することで、もし将来的にShapeに新しい値が追加された場合、TypeScriptはコンパイルエラーを発生させ、型絞り込みの漏れを検出します。


まとめ

この章では、TypeScriptのユニオン型型ガードについて詳しく学びました。ユニオン型は、複数の型を柔軟に扱うための強力な機能であり、型ガードを使って特定の型に絞り込むことで、安全に異なる型のデータを操作することができます。また、typeofinstanceofin演算子、リテラル型を使った絞り込みの方法や、never型を活用した型安全性の向上方法も学びました。