頑健なTypeScriptのコードを書くために「ユニオン型」は欠かせません。
しかし、ユニオン型を使いこなすには「型の絞り込み(Narrowing)」の技術が必須です。
この記事では、そもそもユニオン型とは何か?や、ユニオン型を安全かつ効率的に扱うための強力なテクニック「型ガード」に焦点を当てて解説しています。
typeof、instanceof、in 演算子、そしてユーザー定義型ガードを用いた実践的な手法を学ぶことで、あなたのコードの柔軟性とバグ耐性を劇的に向上させること間違いなしです!
ユニオン型とは何か?
ユニオン型の概要
ユニオン型は、一つの変数や関数の引数が、複数の異なる型を受け取る場合に使う機能です。
ユニオン型を使うことで「この値は型A、または型B、または型Cのいずれか」と宣言することができます。
「ユニオン(Union)」とは、英語で「結合」「合併」「和集合」「連合」といった意味を持つ言葉です。
プログラミングでは「AまたはBのどちらか」という和集合(論理和 OR)の概念を表します。
ユニオン型の使い方
ユニオン型は、各型をパイプ記号 (|) で区切って記述します。パイプ記号は「または (OR)」という意味合いです。
例えば、関数printIdの引数userIdがnumber型かstring型のどちらかをとれるようにする場合は以下のように記述します。
function printId(userId: number | string) {
console.log("あなたのIDは: " + userId);
}
こうすることで、関数を呼び出したときに、数値または文字列であればエラーが出ません。
printId(101); // OK: 数値 (number) を渡せる
printId("abc-456"); // OK: 文字列 (string) を渡せる
// printId(true); // エラー: boolean は指定されていないユニオン型の注意点
ユニオン型の値に対して操作を行う場合、すべての型に共通する操作しかできなくなります。
特定の型でしか機能しない操作を行うとエラーになります。
例えば、「toUpperCase()」というメソッドはstring型には使えますが、number型には使えません。
このため、ユニオン型を使って以下のように記述するとエラーになります。
function printId(id: number | string) {
console.log(id.toUpperCase()); //※エラー
}これを回避するには、if文や typeof などの型ガードを使って、その時点で値がどの型であるかを確定させる処理(型の絞り込み)を使うことが一般的です。
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id * 2);
}
}型ガードとは何か?(型の絞り込み)
型ガードの概要
型ガードとは、変数や値の型をより具体的なものに絞り込むための仕組みです。
特にユニオン型を扱う際に非常に重要な概念です。
型を絞り込む方法は主に以下の3つです。
- typeof 型ガード
- instanceof 型ガード
- in演算子 型ガード
- ユーザー定義 型ガード(カスタム)
型ガードの必要性
ある値が number | string のユニオン型である場合、TypeScriptはコンパイル時には、その値が number なのか string なのかを特定できません。そのため、どちらの型にも存在しないメソッド(例: string にしかない .toUpperCase())を直接呼び出そうとするとエラーになります。
型ガードを使用することで、型固有の操作をすることができます。
typeof 型ガード
typeof 型ガードは、JavaScriptのtypeof演算子を使用して、プリミティブ型(基本型)をチェックする最も一般的な方法です。
typeofの対象となるのは、以下の型です。
stringnumberbooleanbigintsymbolundefinedobjectfunction
例えば、引数が文字列(string型)の場合とそれ以外で型ガードを適用するには以下のようになります。
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id * 2);
}
}instanceof 型ガード
instanceof 型ガードは、JavaScriptの instanceof 演算子を使用して、特定のクラスのインスタンスであるかどうかをチェックする方法です。
instanceofの対象となるのは、クラスです。
例えば、DateFormatterというクラスで、引数valueの値がData型とstring型で処理を分岐する場合は以下のようにします。
class DateFormatter {
format(value: Date | string) {
if (value instanceof Date) {
// このブロック内では、value は確実に Date 型
return value.toLocaleDateString();
}
// ここでは、value は string 型
return value.toString();
}
}in演算子 型ガード
in演算子 型ガードは、in演算子を使って、オブジェクトが特定のプロパティを持っているかチェックする方法です。
インターフェースや型エイリアスのユニオン型を扱う場合によく使います。
in演算子の対象となるのは、オブジェクトです。
インターフェース型や型エイリアスとは何か?については下記をご参考ください。
①【TypeScript】typeエイリアス/型エイリアスとは何か?使い方を実例で解説
②【TypeScript】interface(インターフェース)とは何か?使い方を実例で解説(継承・拡張extends, implements,オプション・任意にする方法)
例えば、引数animalは、Bird型またはFish型のどちらかの型エイリアスをとるとします。
このとき、引数の中にswimがあるなら、Fish型の処理を行い、それ以外はBird型の処理を行う場合は以下のように記述します。
type Bird = { fly: () => void; layEggs: () => void; };
type Fish = { swim: () => void; };
function move(animal: Bird | Fish) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}ユーザー定義型ガード
ユーザー定義型ガードは、独自の関数を作成して、より複雑なロジックで型チェックを行う方法です。
戻り値の型として「 parameterName is Type 」で指定します。
function 関数名(パラメータ名): パラメータ名 is 型{
//渡された引数が、指定した型に一致する場合(true)に処理を実行
}例えば、以下のようにAnimalという、Bird型とFish型のどちらかをとる型エイリアスがあるとします
interface Bird {
kind: 'bird';
fly(): void;
}
interface Fish {
kind: 'fish';
swim(): void;
}
//型エイリアス「Animal」
type Animal = Bird | Fish;isFishという関数を定義したときに、渡された引数の型がFish型と一致する場合のみ処理を実行するには、「引数名 is Fish」というユーザー定義 型ガードを使います。
function isFish(animal: Animal): animal is Fish {
return 'swim' in animal;
}
