続・TypeScriptの`never`と`unknown`
はじめに
先日公開した記事では、部分型関係を導きの糸としてkeyof neverとkeyof unknownの定義を説明しました。存外に多くの方に読まれ著者としては嬉しい限りです。味をしめた今回は、前回の記事でやり残した感のあるneverとunknownそのものの振る舞いについて補足を加えたいと思います。
具体的には、neverとunknownの部分型関係における特殊な立ち位置について、図解を交えた解説を試みます。neverは全ての型の部分型であり、unknownは全ての型の上位型です。この性質を理解すると、|と&にneverやunknownを食わせたときの挙動も理解しやすくなります。また、distributive conditional typeにおけるneverの振る舞いも統一的な解釈が与えられることになります[1]。
なお、前回の記事は読まれていることを前提とさせていただきます。
neverとunknownの基本
never
neverはどんな型に対してもその部分型になります。つまり、以下のコードは問題のないコードです。
function f(x: never) {
const s: string = x
const n: number = x
const b: boolean = x
}
部分型関係は、代入可能性(assignability)とも呼ばれるのでした。neverはstring, number, booleanの部分型なので、never型の項はそれらの変数に代入可能です。もちろんこれらの型に限らず、どんな型の変数にも代入可能です。
ここで、今後の説明のために部分型関係を図で示すときの約束を定めましょう。矢印は部分型関係を表します。また、どんな型もそれ自体の部分型になりますが、説明に必要のない限りは省略します。
unknown
neverとは逆に、unknownは全ての型の上位型です。
declare let x: unknown
x = 1
x = 'hello'
x = true
string, number, booleanはunknownの部分型になるので、unknown型の変数に代入可能です。図に表すと、さっきとは逆向きの矢印が生えています。
|と&におけるneverとunknown
前回の記事で、A | Bを「AとBの上位型の中で一番小さい型」、A & Bを「AとBの部分型の中で一番大きい型」と説明しました。だとすると、neverやunknownが|と&に与えられるとどうなるでしょうか?実はこれも、上記で説明した性質から整合的に解釈可能です。
まず、Tをなんらかの型としたとき、never | T(可換なのでT | neverでもよい)はTと等しいです。これは、T自体が自分自身の上位型かつneverの上位型だからです。
また、unknown | Tはunknownです。unknownは全ての型の上位型なので、unknownはTとunknownの上位型で、それより小さい上位型はありません。
Tにneverやunknown自体が入ってもこの説明は整合的です。例えば、never | neverはneverであり、never | unknownはunknownです。
&についても、neverとunknown, 部分型と上位型を相互に入れ替えることで説明できます。unknown & TはTです。Tは自分自身の部分型で、unknownの部分型だからです。また、never & Tはneverになります。これはもう説明は不要でしょう。
綺麗に反転していますね。neverとunknown、|と&が対となる概念なのが見てとれると思います。
distributive conditional typeにおけるnever
最後に、distributive conditional typeにおけるneverの振る舞いについて触れておきます。distributive conditional typeとは、M<T> = T extends U ? X : Yのようなかたちの条件型のことです。extendsの左辺が型パラメータTそのものの場合、Tにユニオン型が入ると特殊な挙動を示します。
ここでM<string | number>とすると、返ってくる型はstring extends U ? X : Y | number extends U ? X : Yとなります。つまり、ユニオンの各ブランチに対して条件型が適用したものを再度ユニオンするかたちになります。掛け算の分配法則のようなものです(
では、M<never>はどうでしょうか?実はこれはneverとなります。というのも、そうしないと辻褄が合わなくなるからです。M<string | never>について考えてみましょう。string | neverはstringなので、M<string | never>はM<string>と同じ型になって欲しいです。string extends U ? X : Y | never extends U ? X : Yと展開されるM<string | never>が、M<string>、つまりstring extends U ? X : Yに等しくなるためには、never extends U ? X : Yがneverとなる必要があります[2]。よって、M<never>はneverとなるのです。
おわりに
以上、手短ではありましたがneverとunknownについての補足でした。自分でこうやって説明してきて、TypeScriptの型システムは部分型関係を基礎概念にして理解するのが一番わかりやすいような気がしてきました。その点についてのご意見・ご感想お待ちしています。
-
恥ずかしながらdistributive conditional typeにおける
neverの振る舞いは筆者も最近になって知りました。最初はバグを疑いましたが仕様通りの挙動でした。 ↩︎ -
本来は「どんな型
U,X,Yについても」という但し書きがつきます。例えば、M<T> = T extends string ? string : stringとした場合、M<never>がneverでなくともM<string | never>とM<string>はともにstringになります。 ↩︎
Discussion