Gemcook Tech Blog
😎

【TS5.5】filter()で型が絞られる様になったありがたみを噛み締める。

2024/06/11に公開

こんにちは。
今回は、TS5.5にて追加される機能の一つである、inferred type predicates についてみていきながら、今まで行っていた面倒(かつ危険)なことから解放されるありがたみを再確認していきたいと思います。

https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-rc/

いままで(~TS5.4)

さて、本記事の趣旨は「いままでを振り返り、これからをありがたがる」ことですので、まずは不要になる処理について改めてみていきましょう。

よりありがたみを感じたいので、最もシンプルな形((number|null)[]みたいな)ではなく、複数の型のオブジェクトを持つ以下の配列を例にしたいと思います。

type Bird = {
  type: "bird";
  name: string;
  canfly: boolean;
};

type Dog = {
  type: "dog";
  name: string;
};

type Cat = {
  type: "cat";
  name: string;
};

const animals: (Bird | Dog | Cat)[] = [
  { type: "bird", name: "owl", canfly: true },
  { type: "dog", name: "shiba" },
  { type: "bird", name: "hawk", canfly: true },
  { type: "cat", name: "munchikin" },
  { type: "bird", name: "penguin", canfly: false },
  { type: "dog", name: "poodle" },
];

こいつをfilter()して、後続でなんらかのプロパティにアクセスしたい時、
今回でいうと、「dogcat以外の動物に対して何か処理したい」場合、やりたいことをそのままコードにすると以下の様になるんじゃないでしょうか。

理想
const birds = animals.filter(
  (animal) => animal.type !== "dog" && animal.type !== "cat"
);

const description = birds.map((animal) =>
  animal.canfly
    ? `${animal.name}は飛べるよ!`
    : `${animal.name}は飛べません...。`
);

console.log(description) // [owlは飛べるよ!, Hawkは飛べるよ!, penguinは飛べません...。]

5.4以前の環境でも、おそらく誰もが一度は書いたことあるコードでしょう...。
ご存知の通り、~TS5.4だと、期待の通りにはなってくれません...。

いままで起きてたこと...。

では、どうなるのかみていきます。

~5.4
// typeof birds は、(Bird | Dog | Cat)[] のまま...。
const birds = animals.filter(
  (animal) => animal.type !== "dog" && animal.type !== "cat"
);

const description = birds.map((animal) =>
  animal.canfly // ここで 「プロパティ 'canfly' は型 'Bird | Dog | Cat' に存在しません。」と怒られてしまう...。
    ? xxx
    : xxx
);

となり、filter()でしぼり込んだにも関わらず、絞り込んだはずの配列の型は絞り込む前の型のまま。となっています。...イヤですね。

いままでの対応法...。

この問題への対応として isを使用してガードする。 という方法をよくみます。
、当然これは危険な行為であり、相当注意深く型を指定しないと事故が起こる可能性が大きくなるでしょう。
適当に指定するのではなく、可能な限りランタイムエラーの可能性を下げた状態で、isを使用したいです。

良くなさそうな例

~5.4
const birds = animals.filter(
  (animal): animal is Bird => animal.type !== "dog" && animal.type !== "cat"
);

birdでもcatないものをfilter()して、is Birdとして型指定」しています。
安直に指定するのであればこうなるかと思います。

現状は期待通りに動いてくれるでしょう。
しかし、例えばアプリケーションを運用していくに従って、「AnimalFishが追加された。」場合はどうでしょう。

こういうこと
type Bird = {
  type: "bird";
  name: string;
  canfly: boolean;
};

type Dog = {
  type: "dog";
  name: string;
};

type Cat = {
  type: "cat";
  name: string;
};

// NEW!!!
type Fish = { 
  type: "fish";
  name: string;
}

const animals: (Bird | Dog | Cat| Fish)[] = [
  { type: "bird", name: "owl", canfly: true },
  { type: "dog", name: "shiba" },
  { type: "bird", name: "hawk", canfly: true },
  { type: "cat", name: "munchikin" },
  { type: "bird", name: "penguin", canfly: false },
  { type: "dog", name: "poodle" },
  { type: "fish", name: "tuna"} // NEW!!!
];

Fish追加」という修正が入った場合、filter()側の修正も当然必要になってきますが、コンパイルエラーは出ません。is Birdしてるから。ですね。

~5.4
// 本当は、(Bird | Fish)[] だが、Bird[]となる。
const birds = animals.filter(
  (animal): animal is Bird => animal.type !== "dog" && animal.type !== "cat"
);

const description = birds.map((animal) =>
  animal.canfly // Fishの場合はそもそもcanflyがない!!! でも気付けない!!!
    ? `${animal.name}は飛べるよ!`
    : `${animal.name}は飛べません...。`
);

これが怖いため、直接is Birdとするのは避けた方がいいと筆者は考えてています。

じゃあどうすんの?

筆者が好んでいたのは、以下の指定方法でした。

~5.4
const birds = animals.filter(
  (animal): animal is Exclude<typeof animal, { type: "dog" | "cat" }> =>
    animal.type !== "dog" && animal.type !== "cat"
);

一言で言うと、「filter()処理の条件と同様の条件をis側にも書く。」と言うことです。
こうすることで、Fishanimalsに追加された場合は、birdsの型がtype (Bird|Fish)[]となるため、TSがコンパイルエラーとして教えてくれる様になります。

以下の様な、もっと単純な場合でも同様です。

~5.4
const arr: (number|null)[] = [1,null,2,null,null]

/*
const nonNullableArr = arr.filter(
  (item): item is number => !!item
);
*/

const nonNullableArr = arr.filter(
  (item): item is NonNullable<typeof item> => !!item
);

これで少しは型の安全性が増しましたね!!
...「いや、ただfilter()したいだけなのにめんどくさ🤮!!」

これから(TS5.5~)

  • isつけて型を指定しないと...。
  • つけるにしても何か変更があった時に、気づきやすい様にしないと...。
  • そうするにしても、コードが長くなる...。辛い...。

などなど、
5.4の世界線で考えないといけなかったいろんなことを長々と書き、いままでの辛みと面倒臭さを十分に理解していただいたところで、これからの世界線はどうなるのでしょうか...。

~5.4(=== 理想)
// Bird[]となる。
const birds = animals.filter(
  (animal) => animal.type !== "dog" && animal.type !== "cat"
);

const description = birds.map((animal) =>
    animal.canfly // もちろんBirdにはcanflyがあるので、コンパイルエラーも出ない!!
    ? `${animal.name}は飛べるよ!`
    : `${animal.name}は飛べません...。`
);

console.log(description) // [owlは飛べるよ!, Hawkは飛べるよ!, penguinは飛べません...。]

最初に書いた理想の姿そのものです...。
やりたいことがそのままコードになり。余計なものは何もついていません...。

まさに「俺たちが本当にやりたかったこと」が5.5以降はそのまま使える様になります...👏

まとめ

この記事を読んで、このアップデートへのありがたみをより一層感じてくれたら嬉しいなと思います😄
ありがとうございます!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion