🛡️

型ガードを知ったかしない

2021/11/24に公開

型ガードってなによ

百聞は一見にしかずということで簡単な例から紹介します.

type Suuji = string | number;
// いその〜,10を足そうぜ!
const juutasu = (x: Suuji) => x + 10;
console.log(juutasu("5")); // 510

ここにSuujiという型にstringが入るかnumberが入るかわからないという恐ろしいコードがあります.そんなSuujiという型のものを引数として受け取ってから10を足す関数がありますが,これでは,x + 10という処理はxstringだった場合は文字列の結合となってしまいます.

そこで型ガードです.

型を絞り込む(narrowing)ことによって,その型にしかない処理や,プロパティの呼び出しを型安全に書くことができます.

typeofを使った型ガード

const juutasu2 = (x: Suuji) => {
  if (typeof x === "number") {
    return x + 10;
  }
  if (typeof x === "string") {
    return parseInt(x) + 10;
  }
};
console.log(juutasu2("5")); // 15

条件分岐を利用して,そのスコープ内での型を限定していきます.

if (typeof x === "number") {
  return x * 10;
}

上記のスコープではxnumberの場合のみとなり,10を足すことができます.

if (typeof x === "string") {
  return parseInt(x) * 10;
}

こちらのスコープではxstringの場合なので,parseInt()を使ってnumberに変換してから10を足しています.

このように型ガードを利用することで型安全を維持することができます.

わりとよくある例

次は少し実用的な例を見ていきましょう.

type Kensi = {
  name: string;
  hp: number;
  swordSkill: string;
};
type Wizard = {
  name: string;
  hp: number;
  magicSkill: string;
};
type Yusya = Kensi | Wizard;

const attack = (player: Yusya) => {
  console.log(`${player.name}${player.swordSkill}を使った`);
  // WizardにswordSkillというものは存在しないのでエラー
};

いろんな事情があり,攻撃をするための関数は一つにまとめたいのですが,Skillはジョブによって異なっているので,エラーとなります.

解決方法1...typeプロパティを追加して型ガード

こちらは型定義時にtypeというプロパティ(名前は何でもいい)を作ってあげることで,それを使ってどちらの方か判別できるようにするというものです.

type Kensi = {
  type: "kensi"; // 追加
  name: string;
  hp: number;
  swordSkill: string;
};
type Wizard = {
  type: "wizard"; // 追加
  name: string;
  hp: number;
  magicSkill: string;
};

以下のように書くことで型ガードされます.

const attack = (player: Yusya) => {
  if (player.type === "kensi") {
    console.log(`${player.name}${player.swordSkill}を使った`);
  }
  if (player.type === "wizard") {
    console.log(`${player.name}${player.magicSkill}を使った`);
  }
};

解決方法2...inを使用してnarrowingする

inという構文を使うことでそのプロパティが存在するかどうかを判定します.

const attack = (player: Yusya) => {
  if ("swordSkill" in player) {
    console.log(`${player.name}${player.swordSkill}を使った`);
  }
  if ("magicSkill" in player) {
    console.log(`${player.name}${player.magicSkill}を使った`);
  }
};

swordSkillというプロパティが存在していたならKensiの型であるといった具合に型ガードしていきます.

解決方法3...型を判定する関数を自作する

/* 型を判定する関数 */
const isKensi = (player: Yusya): player is Kensi => {
  return "swordSkill" in player; // Kensiにする条件を自作できる
  // return player.type === "kensi" // だから,これでもOK
};

is構文というものを使って戻り値の型を定義することで,narrowingするための関数を作ることができます.このときplayer is Kensiという型はbooleanになりますので,条件を満たす場合はtrueを返すようにカスタマイズしてあげればOKです.

これを使って型ガードします.

const attack4 = (player: Yusya) => {
  if (isKensi(player)) {
    console.log(`${player.name}${player.swordSkill}を使った`);
  } else {
    console.log(`${player.name}${player.magicSkill}を使った`);
  }
};

実際にVSCodeなどでコードを書いて,マウスを乗っけて型が制限されていることを確認すると良いでしょう.

おまけ

型ガードが効かないと思ったらawaitがないやんな話

const attack5 = async (player: Promise<Yusya>) => {
  // const response = player // await忘れるといろいろうまくいかない
  const response = await player;
  if (isKensi(response)) {
    console.log(response.swordSkill);
  } else {
    console.log(response.magicSkill);
  }
};

少々ニッチですが,awaitをつけ忘れてずっと型ガードができなくて悩んでいたことがあったので,おまけで共有します.

参考
https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard

Discussion