どしたん
TypeScript には、型 T
から null
と undefined
を除外するユーティリティ型 NonNullable<T>
が用意されています。過去の動画を参照していたところ、以下のような定義が使われているのを確認しました。
type NonNullable<T> = T extends null | undefined ? never : T;
一方、現在の TypeScript のバージョンでは、次のような実装に変更されています。
type NonNullable<T> = T & {};
この変化について気になったため、変更の経緯を調べてみました。
実装変更の背景
TypeScript 4.8 のリリースノートに、次のような記述がありました。
Another change is that
{}
intersected with any other object type simplifies right down to that object type. That meant that we were able to rewrite NonNullable to just use an intersection with{}
, because{}
& null and{}
& undefined just get tossed away.
TypeScript 4.8 Release Notes
つまり、交差型の簡略化により、T & {}
という形で null
や undefined
を自然に排除できるようになった、ということのようです。
具体的には、以下のような挙動が保証されるようになったとのことでした。
null & {} → never
undefined & {} → never
string & {} → string
この結果、従来のように条件型を使わなくても NonNullable<T>
の目的を達成できるようになった、というわけです。
そもそも変更前の NonNullable<T>
とは
NonNullable<T>
は、型 T
から null
と undefined
を除外するためのユーティリティ型です。従来の定義は次のとおりでした。
type NonNullable<T> = T extends null | undefined ? never : T;
この定義は、分配型条件型(Distributive Conditional Types) という機能を利用しています。
条件型の動作確認
以下のような型を定義するとします。
type A = string extends null | undefined ? null : boolean;
string
は null | undefined
に代入できないため、この評価結果は boolean
になります。
一方、次のような型の場合:
type B = (string | null) extends null | undefined ? null : boolean;
このときは、string | null
全体が null | undefined
に代入可能かどうかを判定するため、分配されず boolean
になるようです。
ただし、以前までのNonNullableのようにジェネリクス型を用いた場合には、分配されるようになっています。
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>; // 結果: string
この処理の流れは以下の通りです。
-
string extends null | undefined
→ false →string
-
null extends null | undefined
→ true →never
-
undefined extends null | undefined
→ true →never
結果として、string | never | never
→ string
となり、null
と undefined
を除外できるわけです。
実装が変更された理由
前述のとおり、TypeScript の型システムの仕様変更によって {}
との交差による型の簡略化が行われました。これにより、以下のような効果が得られるようになったそうです。
null & {} → never
undefined & {} → never
string & {} → string
これによって T & {}
という記述だけで null
や undefined
を排除できるようになり、よりシンプルな形で NonNullable<T>
を定義できるようになったとのことです。
じゃあ無害なんですか
少なくとも、普通に使う分には問題はなさそう。
ありそうなところは、voidに交差型をすると
// 元の実装
type NonNullable_before<T> = T extends null | undefined ? never : T;
// 新しい実装
type NonNullable_new<T> = T & {};
// void型との相互作用
type Test1_before = NonNullable_before<void>; // void
type Test1_new = NonNullable_new<void>; // void & {}
となり、void & {}
となってしまうことだったり、奥深いところで違ってkくる可能性がありそうです。
まぁ少なくとも私が関わってきた極細プロジェクトに影響出ることはないからヨシ!