KameWalk

非nullアサーションでTypeScriptの配列の型情報からundefinedを取り除く

GitHub

配列から undefined を取り除きたい、みたいなことありますよね?僕は仕事中に実際にありました。

const items = ["item1", undefined, "item2"];
const filteredItems = items.filter((item) => !!item);
console.log(filteredItems);
// └──> 中身は["item1", "item2"], 型は(string | undefind)[]のまま

上のようにやると、要素的には undefined が取り除かれてますが、型情報には undefined が残ってしまいます。

この挙動について、はるか昔の時点で GitHubのIssue で話題にあがっているようで(こちらは null を例に挙げてますが)、TypeScript ユーザーからは一刻も早いアプデが望まれているようです。残念ながら 2023 年 10 月においてもこの挙動は残っているのですが、これだと困ることがあるので色々と対処法があるようです。

型ガードを使う

const items = ["item1", undefined, "item2"];
const filteredItems = items.filter((item): item is string => !!item);
console.log(filteredItems);
// └──> 中身は["item1", "item2"], 型もstring[]

ググると一番よく見るのがこれじゃないかなと思います。先述の issue でもこちらがおススメされていました。 矢印の前にそんなに文字書く?みたいに思って、個人的には見た目があんまり好きになれません。

asを使う

const items = ["item1", undefined, "item2"];
const filteredItems = items.filter((item) => !!item) as string[];
console.log(filteredItems);
// └──> 中身は["item1", "item2"], 型もstring[]

うーーむ……。これもあんまり好きになれないんですよね。力こそパワーって感じで強引な感じがします。

非nullアサーションを使う

const items = ["item1", undefined, "item2"];
const filteredItems = items.filter((item) => !!item).map((item) => item!);
console.log(filteredItems);
// └──> 中身は["item1", "item2"], 型もstring[]

どうですか?なんかスマートじゃないですか?

非 null アサーションは型情報から null と undefined を取り除いてくれるというものです。 僕は「初めての TypeScript」を読んで存在を知ったんですけど、配列の型からこうやって undefined を取り除いてるのがググっても見当たらなかったんですよね。

ただしこれ、見た目はスマート(個人の感想です)でも JavaScript にコンパイルしたときにmap((item) => item)が残ります。 つまり、無駄にループさせるので実用性は微妙の一言に尽きると思います。

また、非 null アサーションだけを使うと、型はstring[]なのに中身は何も変わっていないというややこしい事態を引き起こしてしまいます。

const items = ["item1", undefined, "item2"];
const filteredItems = items.map((item) => item!);
console.log(filteredItems);
// └──> 中身は["item1", undefined, "item2"], 型はstring[]

これらのことから、非 null アサーションはasの親戚なのかもしれないと気づきました。 asが型アサーションなんだから早々に気づいてもよさそうなものですが、察しの悪さを発揮してしまいました。

ググっても見当たらなかったのは、みんなこれらの情報を知ってたってことなんでしょうか。 大人しく型ガード使います。

参考