【TypeScript】ユーザー定義型ガードを使ってみた

# はじめに
TypeScriptを触り始めて数ヶ月が経ったのですが、「ユーザー定義型ガード」というものを使う（使わざるをえない）機会に遭遇したので、得た知見をまとめておきます。


# プロパティへのアクセスについて
変数を使って、プロパティにアクセスするときは、以下のようにブラケット記法（`Obj[key]`）を使います。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

const color = "green";

console.log(colorName[color]);
// "緑"
```

上記の例では、変数`color`に`"green"`を代入して、`colorName[color]`でプロパティにアクセスしています。

では次に、仮引数を使って、プロパティにアクセスしてみます。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

const getColorName = (color: string): string | undefined => {
  return colorName[color]; // colorName[color]でコンパイルエラー！
};

console.log(getColorName("green"));
// "緑"
```
上記のように書くと、先ほどとは異なり`colorName[color]`の部分で、以下のようなコンパイルエラーが出てしまいます。

> 型 'string' の式を使用して型 '{ red: string; green: string; blue: string; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
> 型 'string' のパラメーターを持つインデックス シグネチャが型 '{ red: string; green: string; blue: string; }' に見つかりませんでした。ts(7053)

これは、`colorName`のプロパティとして指定できるのは`"red"`or`"green"`or`"blue"`だけであるが、仮引数`color`は文字列であればどんな値も入る可能性があるため、`colorName[color]`の型は保証できない、と言っています。

（`colorName[color]`は`string`か`undefined`にしかなり得ないはずなのですが、TypeScriptはこのことを理解できず、「暗黙的に 'any' 型になります。」と言っている？）


# コンパイルエラーを回避する

一番シンプルな方法は、仮引数`color`の型を、`colorName`のプロパティに限定することです。
```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"

const getColorName = (color: ColorNameKey): string | undefined => {
  return colorName[color];
};
```

しかし、`color`には任意の文字列が入ることを考慮する場合もあるかと思います。
その場合のシンプルな方法は、`colorName[color]`を実行する前に、`color`が`"red"`or`"green"`or`"blue"`であるということを保証することです。<br>
とりあえず、`if (color === "red" || color === "green" || color === "blue")`でやってみます。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

const getColorName = (color: string): string | undefined => {
  if (color === "red" || color === "green" || color === "blue") {
    // ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
    return colorName[color];
  }
  return undefined;
};
```

この方法でも、コンパイルエラーを回避できました。
しかしこの書き方だと、プロパティの数が増えたとき、`if`の条件式も長くなってしまいます。<br>
そこで考えられる代替案としては、`color in colorName`が挙げられます。
こう書くだけで、`color`が`colorName`のプロパティとして存在するかを確認できます。<br>
しかし、`color in colorName`を使った場合、先ほどと同様のコンパイルエラーになってしまいます。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

const getColorName = (color: string): string | undefined => {
  if (color in colorName) {
    // ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
    // が、TypeScriptは上記を理解できていない
    return colorName[color]; // colorName[color]でコンパイルエラー！
  }
  return undefined;
};
```

>型 'string' の式を使用して型 '{ red: string; green: string; blue: string; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
>型 'string' のパラメーターを持つインデックス シグネチャが型 '{ red: string; green: string; blue: string; }' に見つかりませんでした。ts(7053)

`if`ブロック内では、`color`が`"red"`or`"green"`or`"blue"`であることを保証できているのですが、おそらくTypeScriptがこのことを理解しておらず、コンパイルエラーになっていると思われます。

# ユーザー定義型ガードを使う

TypeScriptが理解できないのであれば為す術はないと思うかもしれませんが、「ユーザー定義型ガード」を使うとTypeScriptに理解させることができます。

まず、`color`が`"red"`or`"green"`or`"blue"`であるかを判別している`color in colorName`を関数として切り出します。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

// 判別部分を関数として切り出す
const isColorNameKey = (color: string): boolean => {
  return color in colorName;
};

const getColorName = (color: string): string | undefined => {
  if (isColorNameKey(color)) {
    // ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
    // が、TypeScriptは上記を理解できていない
    return colorName[color]; // colorName[color]でコンパイルエラー！
  }
  return undefined;
};
```

そうしたら、切り抜いた関数の、返り値の型を指定する部分に`key is ColorNameKey`と書きます。
こうすることで、ユーザー定義型ガードとなります。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"

// ユーザー定義型ガード
const isColorNameKey = (color: string): color is ColorNameKey => {
  return color in colorName;
};

const getColorName = (color: string): string | undefined => {
  if (isColorNameKey(color)) {
    // ここでは、変数colorの型は "red" | "green" | "blue" であることが保証されている
    // ユーザー定義型ガードによりTypeScriptも、"red" | "green" | "blue" であるとみなしている
    return colorName[color];
  }
  return undefined;
};
```

上記のユーザー定義型ガードを日本語訳すると以下のような感じになります。
「`color in colorName`が`true`であれば、`color`は`ColorNameKey`型であるとみなす」。

よって、`isColorNameKey(clor)`が`true`となったif文の中では、`color`は`ColorNameKey`型（`"red"`or`"green"`or`"blue"`）であるとTypeScriptはみなしているため、`colorName[color]`でコンパイルエラーが出なくなります。

# 注意点

ユーザー定義型ガードは、型の実態に関わらず、型の情報をプログラマが上書きすることもできてしまうので、注意が必要です。

例としてまず、`getColorName`関数が、必ず文字列を返してほしくなったとして、以下のように修正します。

```typescript
const colorName = {
    red: "赤",
    green: "緑",
    blue: "青"
};

type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"

// ユーザー定義型ガード
const isColorNameKey = (color: string): color is ColorNameKey => {
  return color in colorName;
};

const getColorName = (color: string): string => {
  if (isColorNameKey(color)) {
    // ユーザー定義型ガードのロジックにより、
    // 変数colorの型は "red" | "green" | "blue" であることが保証されている。
    // ユーザー定義型ガードによりTypeScriptも、
    // "red" | "green" | "blue" であるとみなしているため、コンパイルエラーにならない。
    return colorName[color];
  }
  return "定義されていません";
};
```
```typescript
console.log(getColorName("green"));
// "緑"
console.log(getColorName("black"));
// "定義されていません"
```

上記のように修正したのち、ユーザー定義型ガードに誤りがある場合を考えてみます。

今回は、誤って`isColorNameKey()`が常に`true`を返していると想定します。
この場合のユーザー定義型ガードがどうなっているかというと、
「`true`が`true`であれば、`color`は`ColorNameKey`型であるとみなす」つまり、`color`が何であっても`ColorNameKey`型だとみなされてしまいます。

```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

type ColorNameKey = keyof typeof colorName; // "red" | "green" | "blue"

// ユーザー定義型ガード（実際は型ガードできていない）
const isColorNameKey = (color: string): color is ColorNameKey => {
  return true;
};

const getColorName = (color: string): string => {
  if (isColorNameKey(color)) {
    // ユーザー定義型ガードのロジックの誤りにより、
    // 変数colorの型は "red" | "green" | "blue" であることが保証されていない。
    // しかし、ユーザー定義型ガードによりTypeScriptは、
    // "red" | "green" | "blue" であるとみなしているため、コンパイルエラーにならない。
    return colorName[color];
  }
  return "定義されていません";
};
```

```typescript
console.log(getColorName("green"));
// "緑"
console.log(getColorName("black"));
// undefined
//（文字列以外が返却されてしまうが、コンパイルエラーは出ていない、、、）
```

この場合、`color`に`"red"`or`"green"`or`"blue"`以外が指定されても`colorName[color]`が実行されます。しかし、TypeScriptは、if文の中で`color`は`"red"`or`"green"`or`"blue"`であるとみなしているため、コンパイルエラーになりません。

なので、`colorName("black")`が想定外の挙動をするにも関わらず、コンパイルエラーが出ないため、バグに気づきにくくなってしまいます。

ユーザー定義型ガードを使用する際には、ロジックに誤りがないか慎重に確認しましょう。

# おまけ
以下のように、`any`を使うことでも、一応コンパイルエラーは回避できます。

（`any`は可能な限り避けるべきですが）
```typescript
const colorName = {
  red: "赤",
  green: "緑",
  blue: "青"
};

const getColorName = (color: string): string | undefined => {
  return (colorName as any)[color];
};

console.log(getColorName("green"));
// "緑"
```

# 参考書籍

https://gihyo.jp/book/2022/978-4-297-12747-3