Typescript(型ガード)

はじめに

TypeScript には型を推論する機能があり、条件分岐の際に自動的に型を絞り込んでくれます。この仕組みを型ガード(Type Guard)と呼びます。ただし万能ではなく、自動的な絞り込みが機能しないケースもあり、その場合isを使って開発者が TypeScript に型を教えることで解決できます。以下、型を絞り込む方法をいくつか紹介します。

type of

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 に対して型を教えるための機能で、「ユーザー定義型ガード」と呼ばれます。関数の返り値を引数 is Tとアノテートすると、「その関数がtrueを返す場合は引数はTであり、falseを返す場合はTではない」と TypeScript に指示することになります。
以下のisStringrは、返り値をx is numberとアノテートしているため、trueを返した場合はargはnumberであると見做されます。そのため、ifブロックのなかではxがstringに絞り込まれています。

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

この機能を使うことで、型ガードを関数として切り出すことが可能になり、複雑な型ガードを実装することも可能となります。

const isString = (x: number | string): x 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