JavaScript で配列から Falsy な値をフィルターする際に Array.filter(Boolean)
をよく使っていたのですが TypeScript だと型がうまく推論されなかったので TypeScript で配列から Falsy な値を取り除く方法を調べてたメモ
結論
- TypeScript には
is
はあるが、〇〇以外 NOR を表すものがない - TypeScript には現状 (
v4.9.3
現在)NaN
を表す型が無いので.filter(Boolean)
の結果を型情報にすることはできなかった(NaN
はあくまで number 型) - 暗黙的な Falsy は
0
が削除されてしまったり逆にバグの温床になるので、Flasy を除去しなけれなならない状況なのは設計を見直したほうが良いのではないか? NonNullable
でnull
とundefined
を除去する程度に使うのが良さそう
null
と undefined
を除去する Filter
const nonNullableFilter = <T>(value: T): value is NonNullable<T> => (value !== null && value !== undefined); array.filter(nonNullableFilter);
JavaScript の Falsy な値
false
… false キーワード0
... 数値ゼロ-0
... 数値マイナスゼロ0n
… BigInt で、論理値として使用された場合、 Number と同じ規則に従います。 0n は偽値です。""
… 空文字列の値null
…null
- 何も値が存在しないことundefined
…undefined
- プリミティブ値NaN
…NaN
- 非数
Falsy な値は Boolean()
で false
になるので、.filter(Boolean)
で除外できる
[0, -0, 0n, false, null, undefined, NaN, ""].map((item) => Boolean(item)) // => [false, false, false, false, false, false, false, false] [0, -0, 0n, false, null, undefined, NaN, ""].filter(Boolean) // => []
TypeScript で Array.filter(Boolean)
の型推論がうまく行かない
const arr = [0, -0, 0n, false, null, undefined, NaN, ""]; const newArr = arr.filter(Boolean); // => const newArr: (string | number | bigint | boolean | null | undefined)[]
.filter(Boolean)
をしても元の配列の型のまま推論されてしまう
1. 型情報から null
と undefined
を除く
Falsy な値は型が複雑になりそうなので、まずは nullable な型情報を除去してみる。NonNullable
を使うと nullable (null
と undefined
) を除去できる
const arr = [0, -0, 0n, false, null, undefined, NaN, ""]; const newArr = arr.filter((item): item is NonNullable<typeof item> => (item !== null && item !== undefined)); // newArr: NonNullable<string | number | bigint | boolean | null | undefined>[] // => [0, -0, 0n, false, NaN, '', 1, '2']
コールバック関数をユーティリティ化する
毎回 (item): item is NonNullable<typeof item> => (item !== null && item !== undefined)
を書くのは大変なので、ユーティリティ化しておくと便利
const nonNullableFilter = <T>(value: T): value is NonNullable<T> => (value !== null && value !== undefined);
👇 ユーティリティに置換
const arr = [0, -0, 0n, false, null, undefined, NaN, ""]; const newArr = arr.filter(nonNullableFilter); // newArr: NonNullable<string | number | bigint | boolean | null | undefined>[] // => [0, -0, 0n, false, NaN, '', 1, '2']
📝 null 合体演算子 (??
) を使う方法
Null 合体演算子 (
??
) は論理演算子の一種です。この演算子は左辺がnull
またはundefined
の場合に右の値を返し、それ以外の場合に左の値を返します。
cf. Null 合体演算子 (??) - JavaScript | MDN
const arr = [0, -0, 0n, false, null, undefined, NaN, ""]; // filter のコールバックは boolean 型でないとダメなのでそのままエラーになる arr.filter((item): item is NonNullable<typeof item> => (item ?? false)); // Type 'string | number | bigint | boolean' is not assignable to type 'boolean'. // Type 'string' is not assignable to type 'boolean'. const newArr = arr.filter((item): item is NonNullable<typeof item> => !!(item ?? false)); // newArr: NonNullable<string | number | bigint | boolean | null | undefined>[]
ただ値によっては false ?? false
になっていたりで少し気持ち悪い。個人的に明示的に (item !== null && item !== undefined)
としておいた方が見通しが良いのでは?という気もする
2. 型情報からも Falsy な値を除去したい
⚠ NaN は上手く除去できない
NaN
は number 型でしかなく型として定義できない
type Falsy = 0 | -0 | 0n | false | null | undefined | '' | NaN;
=> 'NaN' refers to a value, but is being used as a type here. Did you mean 'typeof NaN'?
NaN
をプリミティブ型として定義することはできないNaN == NaN
がfalse
なのでプリミティブ型にできても、そもそも判定できないNaN
かどうかはisNaN(number)
でしか判定できない
- 型エラーにならないように
typeof NaN
とすると、typeof NaN = number
なので number型 が Falsy に含まれてしまう事になるので良くない
2-1. .filter(Boolean)
を型定義のある関数に置き換える
.filter(Boolean)
は.filter((item) => Boolean(item))
の省略形- コールバック関数を型定義のあるものに置き換える
type Falsy = 0 | -0 | 0n | false | null | undefined | ''; const isTruthy = <T>(x: T | Falsy): x is T => !!x;
const isTruthy = <T>(x: T | Falsy): x is T => !!x;
の解説
- 引数
x
の型がT
またはFalsy
!!x
がtrue
ならx is T
(Falsy
ではない)
2-2. 渡す配列を as const
しなければ正しく判定できない
type Falsy = 0 | -0 | 0n | false | null | undefined | ''; const isTruthy = <T>(x: T | Falsy): x is T => !!x; const result = [0, -0, 0n, false, null, undefined, '', 1, '2'].filter(isTruthy); // => result: (string | number | bigint | true)[] const result = ([0, -0, 0n, false, null, undefined, '', 1, '2'] as const).filter(isTruthy); // => result: (1 | "2")[]
渡す配列を as const
してないと isTruthy
の x: T | Falsy
の部分で number や bigint や boolean という型情報として判定されるので T
扱いになり、0
や 0n
や false
が除去されているか判別されない。
as const
する事でプリミティブな値として渡されるので 0
や 0n
や false
という値として渡されるので Falsy型 の値だと判定されるという仕組み。
NaN
があると as const
しても number 型が帰る
type Falsy = 0 | -0 | 0n | false | null | undefined | ''; const isTruthy = <T>(x: T | Falsy): x is T => !!x; const result = ([0, -0, 0n, false, null, undefined, '', 1, '2', NaN] as const).filter(isTruthy); // => result: (number | "2")[] const result = ([0, -0, 0n, false, null, undefined, '', 1, '2', NaN] as const).filter(isTruthy); // => result: (number | "2")[] const result = ([NaN] as const).filter(isTruthy); // => result: number[]
所管
Array.filyer(Boolean)
で Falsy な値を除去した時に型情報も合わせたかっただけなのですが思った以上に沼だった。
- TypeScript には
is
はあるが、〇〇以外 NOR を表すものがない - NaN は number 型 で
NaN === NaN
がfalse
なので型でキャッチできない
結果的に今の実力では NaN
も除去した型を作ることができなかった。現実的には TypeScript で暗黙的な型変換の Falsy で何かを判定することほぼ無いと思うので NonNullable
で null
と undefined
を取り除けく方法で十分な気がしている。(むしろ暗黙的な Falsy で判定すると意図的な 0
が除去されてしまうとかバグの原因だと思っている)
おわり
[参考]
- Falsy (偽値) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
- Truthy - TypeScript Deep Dive 日本語版
- typescript - Type for non-false, aka truthy - Stack Overflow
- TypeScript で配列から nullable な要素を除去する
- Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors · Issue #28682 · microsoft/TypeScript · GitHub