102
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

TypeScript の"is"と"in"を理解する

TypeScript の構文の中でも、屈指のググラビリティの低さを誇る isin を自分なりにまとめてみました。

"is"

is は TypeScript の型推論を補強する user-defined type guard(ユーザー定義型ガード) で使われます。
unknown 型や、any 型、Union 型の型の絞り込みを行えます。

使用例

例えば、unknown 型を引数に受け取る関数で、もし引数の型が string 型だったら文字列の長さを出力するという処理があるとします。
その場合は、以下のように型を絞り込む必要があります。

const example = (foo: unknown) => {
  if (typeof foo === "string") {
    console.log(foo.length); // fooはstringとして推論される
  }
};

この一つの関数ならば良いのですが、他にも string 型への絞り込みが必要な関数があり、typeof foo === "string" の部分を isString 関数に抽出して汎用的に使いまわせるようにしたいとします。

一見上手くいきそうですが、typeof での型の絞り込みは関数スコープで完結してしまうので、isString が true な場合でも、まだ foo は unknown 型として推論され型の絞り込みが行えません。

const isString = (test: unknown): boolean => {
  return typeof test === "string";
};

const example = (foo: unknown) => {
  if (isString(foo)) {
    console.log(foo.length); // Error fooはまだunknownとして推論される
  }
};

このような時が is の出番です。
is を使うと isString の結果が true の場合は引数で受け取った変数の型は、string 型であるとコンパイラに教えることができます。

const isString = (test: unknown): test is string => {
  return typeof test === "string";
};

const example = (foo: unknown) => {
  if (isString(foo)) {
    console.log(foo.length); // fooはstringとして推論される
  }
};

また、is はオブジェクト型の型の絞り込みにも使えます。
例えば Fish か Bard の Union 型の型をどちらかの型に絞り込みたい時はif((fishOrBard as Bard).fly() !== undefined){}で判定を行えそうです。

しかし、この判定ではまだ TypeScript のコンパイラは fishOrBard を Bard 型と推論してはくれません。

type Bard = {
  fly: () => {
    /*  Do something */
  };
};

type Fish = {
  swim: () => {
    /*  Do something */
  };
};

const example = (fishOrBard: Fish | Bard) => {
  if ((fishOrBard as Bard).fly() !== undefined) {
    console.log(fishOrBard.fly); // Fish | Bard 型として推論され Fishはfly()を持たないので type error
  } else {
    console.log(fishOrBard.swim); // Fish | Bard 型として推論され Bardはswim()を持たないので type error
  }
};

そのような時に (fishOrBard as Bard).fly() !== undefined を関数に切り出し、is で型を指定するとコンパイラは型を推論できるようになります。

const isBard = (test: Fish | Bard): test is Bard => {
  return (test as Bard).fly() !== undefined;
};

const example = (fishOrBard: Fish | Bard) => {
  if (isBard(fishOrBard)) {
    console.log(fishOrBard.fly); // Bard型として推論される
  } else {
    console.log(fishOrBard.swim); // Fish型として推論される
  }
};

注意点

is 自体への型推論は効かないので注意です。間違った指定をしてしまうとコンパイルは通っても実行時エラーになります。

const isString = (test: unknown): test is string => {
  return typeof test === "number";
};

const example = (foo: unknown) => {
  if (isString(foo)) {
    console.log(foo.length); // 型エラーは出ないが、fooはnumber型なので実行時Errorが発生する
  }
};

(その挙動の解決策としてこちらの記事で便利なライブラリが紹介されていました。)

"in"

in は コンテキストによって 2 つの意味を持つ構文です。
一つは JS にもあるオブジェクトが特定のプロパティを持つか判定するもの。型の絞り込みに使えます。
もう一つは keyof 構文と組み合わせて mapped type の定義に使えます。

使用例

型の絞り込み

前述の is で オブジェクトの型判定に(fishOrBard as Bard).fly() !== undefined) を使いましたが、こちらは in で代替することができます

in を使えば判定処理を関数に切り出し is で type predicate を記述する必要もありません。

const example = (fishOrBard: Fish | Bard) => {
  // fishOrBardは'fly'というプロパティを持っているかどうかの判定
  if ("fly" in fishOrBard) {
    console.log(fishOrBard.fly); // Bard型として推論される
  } else {
    console.log(fishOrBard.swim); // Fish型として推論される
  }
};

mapped type での利用

keyof 構文と組み合わせて mapped type で新しい型を定義できます。
keyof プロパティ名の Union 型を取り出し in で反復処理するイメージですかね。

type Dog = {
  name: string;
  run: () => void;
};

type PartialDog = {
  [P in keyof Dog]?: Dog[P];
};
// PartialDogは以下の型になる
// { name?: string, run?: () => void}

終わりに

以上 TypeScript の isin 構文の説明でした。
分かってみると何かと便利に使えそうですね。
自分自信まだまだ勉強中なので誤った記述などあれば、コメントや修正リクエストで教えて頂けると嬉しいです!!

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
102
Help us understand the problem. What are the problem?