続・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
、|
と&
が対となる概念なのが見てとれると思います。
never
distributive conditional typeにおける最後に、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