🎉

型ガード

2020/09/30に公開

はじめに

TypeScriptには型を推論する機能があり、条件分岐の際に自動的に型を絞り込んでくれます。この仕組みを型ガード(Type Guard)と呼びます。
以下、型を絞り込む方法をいくつか紹介します。


typeof演算子

type ofは純粋なユニオン型との相性が良いです。つまり純粋ではないユニオン型(例えばリテラル型とユニオン型との複合)にtype ofでの型ガードは避けるべきです。エラーが発生します。

function doSomething(x: 'max' | 7) {
  if (typeof x === 'max') { //エラー(型 '"string" | "number" | "bigint" |  "boolean" | "symbol" | "undefined" | "object" | "function"' と '"max"' には重複がないため、この条件は常に 'false' を返します。)
    console.log(x.toUpperCase());
  } else {
    console.log(x.toFixed(2));
  }
}

エラー内容にも記載してくれていますが、type ofはJavaScript上でのデータ型を調べるものであるためTypeScript対応のリテラル型による条件分岐には対応できません。

よって以下のように純粋なユニオン型(ここではnumber型かstring型)でtype ofを使います。

//型ガードfunction doSomething(x: number | string) {
  if (typeof x === "string") {
    console.log(x.toUpperCase());
  } else {
    console.log(x.toFixed(2));
  }
}
doSomething('max')//MAX
doSomething(7)//7.00

上記の関数は、引数としてnumberかstringを受け取ります。
xの時点では、ユニオン型のためどちらなのかは判断できません。
ですがif文でxのtypeofをチェックしており、ifブロックのなかではxはstringであることが確定しています。そして、TypeScriptはそれを認識しており、xはstringだと自動的に推論してくれます。そして、elseブロックのなかではxはstringではないことが確定しているため、型が絞り込まれ、numberであると推論されます。


in演算子

JavaScriptにはinというキーワードがあります。inを使うとオブジェクトにプロパティが存在するかどうかをチェックすることができます。
以下インターフェースを用いた型ガードです。

interface Person {
  name: string;
  age: number;
}
interface Book {
  name: string;
  price: number;
}

//型ガード
function doStuff(arg: Person | Book) {
  if ("age" in arg) {//ageプロパティはPersonのみ存在(Bookにはない)
    console.log(arg.name);
  } else {
    console.log(arg.price);
  }
}
doStuff({ name: "max", age: 22 });
doStuff({ name: "チャート式", age: 2000 });

instanceof

クラスを使っている場合にはinstanceofという演算子を使って型ガードを表現できます。

class Car {
  drive() {
    console.log("運転中...");
  }
}
class Truck {
  drive() {
    console.log("トラックを運転中");
  }
  lpadCargo(amount: number) {
    console.log("荷物を載せています" + amount);
  }
}
const v1 = new Car();
const v2 = new Truck();

//型ガード
function useVehicle(vehicle: Car | Truck) {
  vehicle.drive();
  if (vehicle instanceof Truck) {
    vehicle.lpadCargo(1000);
  }
}
useVehicle(v1);//運転中...
useVehicle(v2);//トラックを運転中
               //荷物を載せています1000

このinstanceof演算子はJavaScriptに組み込まれているものです。
JavaScriptはTruckという型が存在することはわかりませんが、constructor関数またはクラスとしてのTruckは知っています。
したがって、JavaScriptはTruckというコンストラクタ関数によって作られたオブジェクトかどうかということをinstanceofで判断することができます。


ここまでの型ガードの問題点

これまでtypeofやinやinstanceof使った型ガードの例を見てきましたが共通して型の絞り込みに関する問題点があります。
それは、現在のスコープ内で変数の方を絞り込む程度の威力しかないということです。そのスコープを離れると、型の絞り込みは、新しいスコープに引き継がれません。
以下のコードで確認しましょう。

function doSomething(x: number | string) {
  const isString = (x: number | string) => typeof x === "string";
  if (isString(x)) {
    console.log(x.toUpperCase());//エラー(プロパティ 'toUpperCase' は型 'string | number' に存在しません。)
  } else {
    console.log(x.toFixed(2));
  }
}

typeof演算子でのコードを少しいじりました。
条件分岐でtypeofを使っていたところをisStringという変数に格納してから条件分岐でstring型を絞り込もうとしています。
しかしxの型がstring型に絞り込むことができず(引数の型で定義したnumber型かstring型のユニオン型のまま)string特有のメソッドを使用すると怒られます。
このように一旦スコープから離れる(isStringという変数に格納)と、型ガードのはうまく機能しません。


ユーザー定義型ガード

is演算子を使うことで、条件式を切り出すこと(スコープから離れること)が可能となります。is演算子は、開発者が TypeScript に対して型を教えるための機能で、ユーザー定義型ガードと呼ばれます。関数の返り値を引数 X is Tとアノテートすると、条件がtrueを返す場合はXはTであり、falseを返す場合はTではないTypeScriptに指示することになります。

typeofとユーザー定義型ガード

function doSomething(x: number | string) {
  const isString = (x: number | string): x is string => typeof x === "string";
  if (isString(x)) {
    console.log(x.toUpperCase());
  } else {
    console.log(x.toFixed(2));
  }
}
doSomething("max"); //MAX

isStringは、返り値をx is numberとアノテートしているため、trueを返した場合はargはnumberであると見做されます。そのため、ifブロックのなかではxがstringに絞り込まれています。
この機能を使うことで、型ガードを関数として切り出すことが可能になり、複雑な型ガードを実装することも可能となります。

//is演算子による型ガードconst isString = (arg: number | string): arg is string => typeof x === "string";
function doSomething(x: number | string) {
  if (isString(x)) {
    console.log(x.toUpperCase());
  } else {
    console.log(x.toFixed(2));
  }
}
doSomething("max"); //MAX

in演算子とユーザー定義型ガード

interface Person {
  name: string;
  age: number;
}
interface Book {
  name: string;
  price: number;
}
//is演算子による型ガード
const isPerson = (arg: Person | Book): arg is Person => "age" in arg;
const doStuff = (x: Person | Book) => {
  if (isPerson(x)) {
    console.log(x.age);
  } else {
    console.log(x.price);
  }
};
doStuff({ name: "max", age: 22 });
doStuff({ name: "チャート式", price: 2000 });

instanceof演算子とユーザー定義型ガード

class Car {
  drive() {
    console.log("運転中...");
  }
}
class Truck {
  drive() {
    console.log("トラックを運転中");
  }
  lpadCargo(amount: number) {
    console.log("荷物を載せています" + amount);
  }
}
const v1 = new Car();
const v2 = new Truck();

//is演算子による型ガード
const isTruck = (arg: Car | Truck): arg is Truck => arg instanceof Truck;
function useVehicle(vehicle: Car | Truck) {
  vehicle.drive();
  if (isTruck(vehicle)) {
    vehicle.lpadCargo(1000);
  }
}
useVehicle(v1); //運転中...
useVehicle(v2); //トラックを運転中
//荷物を載せています1000

Discussion