🍭

TypeScript で配列から nullable な要素を除去する

2020/12/07に公開

はじめに

この記事は #EveOneZenn (Everyday One Zenn) vol.13 です。

配列から nullable な要素を除去するフィルタを TypeScript で実装する方法をまとめます。

前回:
https://zenn.dev/lollipop_onl/articles/eoz-nuxt-dev-memory-leak

TypeScript 5.5 でビルトインサポートになります

2024/03/18 追加

2024/06/22 追記

TypeScript 5.5 にて Array.filter による型の絞り込みがビルトインでサポートされるようになりました。

https://github.com/microsoft/TypeScript/pull/57465

この記事で紹介している例も、 nonNullable に相当するユーザー定義の型ガードがなくても期待する型の絞り込みが行えます。

const arr = [0, 1, 'hello', false, null, undefined];

const result = arr.filter((item) => item != null);
// js: [0, 1, 'hello', false]
// ts: (string | number | boolean)[] 🎉

TypeScript Playground

配列から nullable な値を除去する

JavaScript で配列から nullable (undefinednull) を除去する方法としては、 Array.prototype.filter メソッドで要素が nullable でないかチェックするのが一般的かと思います。

const arr = [0, 1, 'hello', false, null, undefined];

arr.filter((item) => item != null);
// js: [0, 1, 'hello', false]

JavaScript ではこの実装で問題ありませんが、 TypeScript で同様のコードを実行すると型情報上に nullable が残ってしまいます。

const arr = [0, 1, 'hello', false, null, undefined];
// ts: (number | string | boolean | null | undefined)[]

arr.filter((item) => item != null);
// js: [0, 1, 'hello', false]
// ts: (number | string | boolean | null | undefined)[]

型情報からも nullable な要素を除去する

Array.prototype.filter メソッドのビルトイン型からの推論では、型情報から nullable を除去することができませんでした。

そこで、ユーザー定義の Type Guard を使用して型ガードを明示的に定義します。
ユーザー定義の Type Guard は a is Ba が変数、 B が型)で変数 a の型を B と定義し直すことができます。

const arr = [0, 1, 'hello', false, null, undefined];
// ts: (number | string | boolean | null | undefined)[]

arr.filter((item): item is NonNullable<typeof item> => item != null);
// js: [0, 1, 'hello', false]
// ts: (number | string | boolean)[]

NonNullable 型は指定した型から nullundefined を除去するものです。

これで、ランタイムと型情報いずれからも nullable を除去するフィルタが作成できました。

ユーティリティ化する

上記で紹介したフィルタ関数はユーティリティ化することができます。

const nonNullable = <T>(value: T): value is NonNullable<T> => value != null;

使い所としては例えば、 Array.prototype.mapundefined を返しうるとき、最終的に undefined を除いた配列を返したいケースです。

const users = [
  { id: 0, name: 'Hanako', active: true },
  { id: 1, name: 'Taro', active: false },
  { id: 2, name: 'Natsumi', active: true },
];

// active: true であるユーザーの名前の配列
const activeUserNames = users
  .map((user): string | undefined => {
    if (!user.active) {
      return;
    }

    return user.name;
  })
  .filter(nonNullable);
// js: ['Hanako', 'Natsumi']
// ts: string[]

ユーザー定義の Type Guard を使う際の注意

ユーザー定義の Type Guard は実際の処理で返される値と型定義がずれることがあります。

たとえば、次のコードはフィルタ内で Falsy な要素を除去していますが、型定義上は nullable を除去していることになっています。
このコードはエラーが発生しません。

const arr = [0, 1, 'hello', false, null, undefined] as const;
// ts: (number | string | boolean | null | undefined)[]

arr.filter((item): item is NonNullable<typeof item> => !!item);
// js: [1, 'hello']
// ts: (number | string | boolean)[]

JavaScript ランタイムでは false0 も除去されていますが、型定義上では boolean も返ることになってしまっています。

このような実装と型の乖離を起こさないよう、ユーザー定義の Type Guard を使用する際は注意して実装しましょう。

参考

GitHubで編集を提案

Discussion