🍄

TypeScriptにおいて型ガードとis演算子を利用して失った型を復活させるぞ!

2023/12/14に公開

CastingONE Advent Calendar 2023 14 日目の記事です!

はじめに

表題の通りなんですが、TypeScript をやっていて、どーーーーしても型がanyunknownstring|number|null|number[]みたいな意味わからないユニオン型になってしまうことはありませんか? 型を指定したいっていうのはもちろんなので気をつけたいところではありますが、それでも自由な型が入ってきてしまう可能性がある場合の実装って困りませんか?私は困りました!

先日業務をしていて、自由に入力できる入力項目を扱ったことがありました。その項目自体は、string でくるのか number でくるのか配列でくるのかがわからない、という感じです。しかし、来た項目の型の種類によって色々と別々に処理はしなきゃいけないよね、みたいな。

たとえば、この項目を利用して画面に表示したいのであれば、numberできた時は 3 桁ごとにカンマ区切りにして suffix として記号をつけたい、stringできた時はそのまま表示したい、number[]の時にはその数字を ID にしている情報を利用して表示内容を確定させたい({id: number, name: "hoge"}みたいな情報が他にあって…)。

で、この項目を使って後続で色々と処理を行いたいのであれば、項目にはぜひとも型を復活させたいと思いました。

TypeScript は基本的に、型の可能性を厳密に確認し、少しでも可能性のある型の一部がそのメソッドを持っていなかったりすると、エラー可能性があるということで教えてくれます。

そのため、ユニオンなどで型の可能性が広がってくると、あらゆるパターンから厳格なチェックが入り、型固有のメソッドを利用できなくなることが発生してきます。

このように、曖昧な型情報にならざるを得なくなってしまった値について、再度特定の型として復活させる方法がありました!それが型ガードと is 演算子(ユーザー定義型ガード)です。

業務でとってもお世話になったので、今回はこの 2 つを利用して失った型を復活させる方法をおさらいしてみました。

型ガードとは

まずは型ガードがどういうものかについてです。

Type Guard を使用すると、条件ブロック内のオブジェクトの型を制限することができます。

とある通り、型ガードを利用することでそのブロック内において型の絞り込みができるようになります。

function getItem(item: unknown): string {
  // このスコープ内はstringとして扱えるように!
  if (typeof item === "string") {
    return item.toUpperCase();
  }
  if (typeof item === "number") {
    return item.toString();
  }
  // ただしこれらは<'item''は 'unknown' 型です。ts(18046)>というエラーに
  // return item.toUpperCase();
  // return item.toString();
  return "";
}

例のように、絞り込んだスコープ内ではその型として認識できるようになります。

型ガードの種類としては以下 3 つが主に挙げられます。詳しくは上記の参考サイトを見てみてください。個人的には、結局はシンプルな形であればnumberstringを比較するtypeofをよく使うなというイメージでした。

  • typeof
  • instanceof
  • in

is 演算子(ユーザー定義型ガード)とは

先ほどのサンプルに示した通り、型ガードはそのブロックにおける条件の絞り込みに最適ではありますが、ブロックを外れればその効果がなくなってしまいます。

先ほどのようにgetItemのようにシンプルであれば良いですが、時には絞り込んだ型に対して処理を続けたり他の情報と比較したりしたくなることが想定されます。スコープを抜けた場所でその型を復活させたいということですね。以下のような感じで実装してみます。

function isItemString(item: unknown): boolean {
  // ブロックで囲むと、型が絞り込まれる!
  if (typeof item === "string") {
    return true;
  }
  return false;
}

const item: unknown = "item";
if (isItemString(item)) {
  // <'item''は 'unknown' 型です。ts(18046)>というエラー
  // 絞り込んだはずなのにunknown型に戻ってる!
  console.log(item.toUpperCase());
}

というわけで、このように型ガードの効力は一時的であり、ブロックの外にまで型情報をうまく渡すことはできません!せっかく絞ったのに…。
こんなケースで役に立つのが、is 演算子(ユーザー定義型ガード)です。こんな感じで書きます。

function isItemString(item: unknown): item is string {
  // ブロックで囲むと、型が絞り込まれる!
  if (typeof item === "string") {
    return true;
  }
  return false;
}

const item: unknown = "item";
if (isItemString(item)) {
  // 型がstringになっている!
  console.log(item.toUpperCase());
}

item is stringという記述の通り、TypeScript に対して「この関数を true で抜けた場合は、itemstring ですよ確実に。だから型はstringに型を変更してね!」と教えることを意味します。なので、is演算子 でありユーザー定義という名称なのだと思います。

今回はシンプルなstringの検証のみでしたが、使い方次第ではオブジェクトの型を比較して複雑な型をunknownから復活させることもできるというわけです。便利ですね。

ちなみに: any と unknown

サンプルではanyではなくunknownの型を採用していますが、個人的おすすめがunknownなので意図的にそうしてます(とはいってもケース的にはanyになることも多いかと思いますが…)。

anyunknownにどういった違いがあるかについては、参考の記事がとてもわかりやすく教えてくださっています。

anyが TypeScript が「型のチェックができない。わからん」と判断した時に入れるものであるのに対し、unknown型は明示的に「この型はなんなのかわからないな…」という時に利用される型安全なany型です。

コードの可読性という意味でもunknown型になっている方が意図がイメージしやすいです。また、何者かわからない状態から型ガードと is 演算子を絞り込んできちんと型を復活させるという意味では、一旦 unknown 定義をして型が不明という意味を持たせるのが良いのかなと思います。

型ガードと is 演算子を併用することの落とし穴

型ガードと is 演算子、すごい便利ですよね。すごいのでいざっていう時に重宝したくなるものなのですが、この方法には 1 つ落とし穴があります。それは、型ガードや is 演算子を間違えると、TypeScript に対して実態と異なる型の情報を渡してしまい、全く思わぬところでエラーになる可能性があるということです。

型ガードと is 演算子を利用すると、いつも私たちに TypeScript が型を教えてくれるのとは逆に、いわば私たちが TypeScript に「これはこういう型だよ」と教えていくということになるイメージです。

どんな形であれ、情報は一度anyunknown、複雑なユニオン型となっており、さまざまな型の可能性をはらんだ状態であることは間違いありません。型ガードの利用についてドキュメントなどをよく確認した上で is 演算子と併用し、思わぬところで実行時にエラーにならないようにしたいものだなと思っています。

いつもの

私は普段、Go と TypeScript をメインでさわっているのですが、型に関しては TypeScript は本当に難しいなと思っています。日々勉強中です。でも型が合ったり綺麗な実装にできたりすると、嬉しくなったりもするのでやっぱり好きだな〜と思ってます!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion