🐈

続・TypeScriptの`never`と`unknown`

2024/09/28に公開

はじめに

先日公開した記事では、部分型関係を導きの糸としてkeyof neverkeyof unknownの定義を説明しました。存外に多くの方に読まれ著者としては嬉しい限りです。味をしめた今回は、前回の記事でやり残した感のあるneverunknownそのものの振る舞いについて補足を加えたいと思います。

具体的には、neverunknownの部分型関係における特殊な立ち位置について、図解を交えた解説を試みます。neverは全ての型の部分型であり、unknownは全ての型の上位型です。この性質を理解すると、|&neverunknownを食わせたときの挙動も理解しやすくなります。また、distributive conditional typeにおけるneverの振る舞いも統一的な解釈が与えられることになります[1]

なお、前回の記事は読まれていることを前提とさせていただきます。

neverunknownの基本

never

neverはどんな型に対してもその部分型になります。つまり、以下のコードは問題のないコードです。

function f(x: never) {
  const s: string = x
  const n: number = x
  const b: boolean = x
}

部分型関係は、代入可能性(assignability)とも呼ばれるのでした。neverstring, number, booleanの部分型なので、never型の項はそれらの変数に代入可能です。もちろんこれらの型に限らず、どんな型の変数にも代入可能です。

ここで、今後の説明のために部分型関係を図で示すときの約束を定めましょう。矢印は部分型関係を表します。また、どんな型もそれ自体の部分型になりますが、説明に必要のない限りは省略します。

unknown

neverとは逆に、unknownは全ての型の上位型です。

declare let x: unknown
x = 1
x = 'hello'
x = true

string, number, booleanunknownの部分型になるので、unknown型の変数に代入可能です。図に表すと、さっきとは逆向きの矢印が生えています。

|&におけるneverunknown

前回の記事で、A | Bを「ABの上位型の中で一番小さい型」、A & Bを「ABの部分型の中で一番大きい型」と説明しました。だとすると、neverunknown|&に与えられるとどうなるでしょうか?実はこれも、上記で説明した性質から整合的に解釈可能です。

まず、Tをなんらかの型としたとき、never | T(可換なのでT | neverでもよい)はTと等しいです。これは、T自体が自分自身の上位型かつneverの上位型だからです。

また、unknown | Tunknownです。unknownは全ての型の上位型なので、unknownTunknownの上位型で、それより小さい上位型はありません。

Tneverunknown自体が入ってもこの説明は整合的です。例えば、never | neverneverであり、never | unknownunknownです。

&についても、neverunknown, 部分型と上位型を相互に入れ替えることで説明できます。unknown & TTです。Tは自分自身の部分型で、unknownの部分型だからです。また、never & Tneverになります。これはもう説明は不要でしょう。

綺麗に反転していますね。neverunknown|&が対となる概念なのが見てとれると思います。

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となります。つまり、ユニオンの各ブランチに対して条件型が適用したものを再度ユニオンするかたちになります。掛け算の分配法則のようなものです(a(b + c) = a\cdot b + a\cdot c)。

では、M<never>はどうでしょうか?実はこれはneverとなります。というのも、そうしないと辻褄が合わなくなるからです。M<string | never>について考えてみましょう。string | neverstringなので、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 : Yneverとなる必要があります[2]。よって、M<never>neverとなるのです。a(b + 0) = a\cdot bとなるのは、a \cdot 0 = 0だからなのと同様です。

おわりに

以上、手短ではありましたがneverunknownについての補足でした。自分でこうやって説明してきて、TypeScriptの型システムは部分型関係を基礎概念にして理解するのが一番わかりやすいような気がしてきました。その点についてのご意見・ご感想お待ちしています。

脚注
  1. 恥ずかしながらdistributive conditional typeにおけるneverの振る舞いは筆者も最近になって知りました。最初はバグを疑いましたが仕様通りの挙動でした。 ↩︎

  2. 本来は「どんな型U, X, Yについても」という但し書きがつきます。例えば、M<T> = T extends string ? string : stringとした場合、M<never>neverでなくともM<string | never>M<string>はともにstringになります。 ↩︎

Discussion