🏹

「型=集合」だけでは解けない謎。中学数学の直感を捨てて、TypeScriptの keyof never を理解する

に公開
2025/12/09

はじめに

プログラミングで「型(Type)」を学ぶとき、私たちは無意識のうちに 中学や高校の数学で習った「集合」 のイメージで理解しようとします。

  • boolean 型は true と false という2つの要素が入った集合。
  • string 型は無限にある文字列の集合。
  • そして never 型は、要素が一つもない「空集合(\emptyset)」。

このメンタルモデルは非常に優秀で、TypeScriptの学習の9割くらいはこの直感でうまくいきます。しかし、ふとした瞬間にこの直感が通用しない「不可解な挙動」に出くわすことがあります。

その代表例が、keyof never です。

type Result = keyof never;
// Result: string | number | symbol

集合論で考えれば、never は空集合です。「中身が何もない空っぽの箱」の鍵(key)なんて、当然「存在しない(never)」はずですよね?
しかし、TypeScriptが出した答えは真逆でした。string | number | symbol、つまり 「オブジェクトのキーになりうる全ての型」 が返ってきたのです。

「空集合のキーが、なぜ全体集合になるのか?」

このパラドックスは、「型を丸い円(ベン図)のような集合」として捉えている限り、納得のいく答えには辿り着けません。ここで必要になるのが、少しだけ視点を変えて 「矢印(関係性)」 で世界を見る考え方です。

今回は、集合論の直感で躓いてしまった人に向けて、 圏論(Category Theory) という数学の考え方を少しだけ借りて、このミステリーを解き明かしていきます。「圏論」と聞くと身構えてしまうかもしれませんが、難しい数式は使いません。「丸(集合)」を「矢印」に置き換えるだけで、この謎が驚くほどスッキリ理解できるようになります。

1. 直感の罠:型を「集合」として見てみる

まず、私たちが直感的にイメージしやすい「集合(ベン図)」のアプローチで考えてみましょう。
ここにおなじみの Animal と Dog があります。

interface Animal {
    name: string; // 名前
    age: number;  // 年齢
}

interface Dog extends Animal {
    breed: string; // 犬種(プードル・チワワなど)
}

これを集合として捉えると、Dog は Animal の一種ですから、Dog 集合は Animal 集合の部分集合になります(Dog は Animal に包含されます)。

そして、never です。 never は値が存在しない型ですから、 どの型の「要素」にもなりません。 しかし同時に、空集合はあらゆる集合の部分集合であるため、 あらゆる型の「内側」に含まれる最も小さな集合 として位置づけられます。

【図1:型の集合(包含関係)】 ※ここでは説明を単純にするため、unknown / Animal / Dog / never だけを取り出して描いています。
直感的には、外側が広く、内側に行くほど狭くなります。

この図を見ると、次のように考えたくなります。

「Dog は Animal の内側にある(小さい)。never はもっと小さい(空っぽ)。
だから、never のキーも一番小さく(空に)なるはずだ!」

これが、集合論的な直感の罠です。

2. 反転する世界:キーの集合を見てみる

では、実際に keyof をとって「キーの集合」を見てみましょう。

type AnimalKeys = keyof Animal; // "name" | "age"
type DogKeys    = keyof Dog;    // "name" | "age" | "breed"

お気づきでしょうか?
型の集合としては小さかった(内側にあった)Dog ですが、キーの集合としては Animal よりも大きくなっています。

  • Animal のキー:2個
  • Dog のキー:3個

これを図にすると、包含関係が完全に逆転していることがわかります。

【図2:キーの集合】
図1では内側にあった Dog 由来のものが、外側に来て、Animal を飲み込んでいます。

  • 図1(型の集合): Dog \subset Animal
  • 図2(キーの集合): keyof Animal \subset keyof Dog

「具体的な型(狭い集合)になればなるほど、プロパティの定義は増えるため、キーの集合は広くなる」

これが直感と逆になる理由です。

3. 圏論の矢印で理解する

この「逆転」を正しく捉えるためには、視点を変える必要があります。「円の大きさ(ベン図)」を見るのをやめて、「矢印の向き(関係性)」 で世界を見てみるのです。

少し抽象的な話になりますが、ここが理解できると never の謎が一気に解けます。

Step 1: ベン図を「矢印」に翻訳して「世界」を作る

まず、私たちがこれまで見てきた「ベン図の包含関係」を、シンプルな「矢印」のルールに書き換えてみましょう。

ルールはたった一つです。
「A は B に含まれる(A \subset B)」なら、「A から B へ矢印を引く(A \to B)」。これだけです。

このルールを使って、2つの「世界(地図)」を描いてみます。

① 「型の世界」を描く

まず、図1(型のベン図) をこのルールで書き換えます。
Dog は Animal の内側にありました。つまり、矢印は Dog から Animal へ伸びます。

  • 関係: Dog \subset Animal
  • 矢印: Dog \xrightarrow{extends} Animal

このように、型同士の包含関係をすべて矢印でつないだ地図、これをここでは 「型の世界」 と呼ぶことにします。

② 「キーの世界」を描く

次に、図2(キーのベン図) も同じルールで書き換えます。
ここでは Animal のキー(2個)が、Dog のキー(3個)の内側にありました。
包含関係が逆なので、当然、矢印の向きも逆になります。

  • 関係: keyof Animal \subset keyof Dog
  • 矢印: keyof\ Animal \xrightarrow{extends} keyof\ Dog

こちらは、キーの集合同士の包含関係を矢印でつないだ地図、つまり 「キーの世界」 です。

Step 2: keyof は 2つの世界をつなぐ「橋」

ここで重要なのが、keyof という操作の正体です。
単にプロパティ名を取り出すだけではありません。keyof は、「型の世界」の住人を、「キーの世界」の住人へと移動させる「橋渡し」 の役割を果たしているのです。

この「橋」を渡ると、不思議なことが起こります。
先ほど見たように、「型の世界」での矢印の向きが、「キーの世界」に着いた途端にすべて逆向き(反転)になってしまうのです。

これを図で確認してみましょう。

【図3:keyof による世界の対応と反転】
下が「型の世界」、上が「キーの世界」です。真ん中の点線が keyof という橋渡しを表しています。

  • 左側(型の世界): 下から上へ(Dog \to Animal)
  • 右側(キーの世界): 上から下へ(keyof Animal \to keyof Dog)
  • 真ん中の点線(keyof): 型の世界からキーの世界へ橋を渡ると、上下関係がひっくり返る。

Step 3: カメラを引いて「世界の果て」を見る

さて、Step 2 までは Dog と Animal という、世界のほんの一部(中腹)だけを見ていました。
ここで少しカメラを引いて、「型の世界」の全体像(ロングショット) を眺めてみましょう。

本来、この世界には string や boolean、Date、そしてあなたが定義するあらゆるインターフェースなど、無数の型が存在しています。Dog や Animal は、その広大なネットワークの中からたまたま拾い上げた2つの点に過ぎません。

しかし、どんなに型が増えても、すべての矢印が従う「大きな流れ」は変わりません。
この世界には、「絶対的な始点」 と 「絶対的な終点」 が存在します。

1. 型の世界の全体像

  • 絶対的な終点: unknown
    すべての矢印が最終的に行き着く場所です。Dog も Animal も、その他のどんな型も、矢印をたどっていけば最後は必ずここに到達します。ここから先に出る矢印はありません。

  • 絶対的な始点: never
    すべての矢印の始まりの場所です。ここには何も入ってきませんが、ここからはあらゆる型に向けて矢印が伸びていきます。

これを図にすると、Dog や Animal は、この壮大な川の流れに浮かぶ、単なる通過点であることが分かります。

【図4:型の世界の全体像】
※ Dog や Animal 以外にも、無数の型(...)がこの流れの中に存在しています。

型の世界では、すべての矢印は never(始点)から出発し、無数の型を経由しながら、最終的に unknown(終点)へと向かいます(もちろん never \xrightarrow{extends} unknown の直通ルートもあります)。

2. キーの世界へ移動

では、この全体像を保ったまま、keyof の橋を渡って 「キーの世界」 へ移動しましょう。
Step 2 のルールを思い出してください。「橋を渡ると、矢印の向きはすべて逆転」します。

すると、どうなるでしょうか?
世界の「中腹」にいた Dog や Animal の順序がひっくり返るだけでなく、「始点」と「終点」の役割が完全に入れ替わります。

  • unknown の逆転:
    「絶対的な終点(入ってくる場所)」だった unknown は、矢印が逆流することで 「絶対的な始点(出ていく場所)」 に変わります。
    (実際、keyof unknown は never になります)

  • never の逆転:
    ここが核心です。「絶対的な始点(出ていく場所)」だった never は、すべての矢印が集まってくる 「絶対的な終点(行き着く場所)」 へと姿を変えます。

【図5:キーの世界での天地逆転】

ここで一つ、疑問が浮かぶかもしれません。

「型の世界の終点が unknown なら、キーの世界の終点も unknown になるんじゃないの?」

しかし、ここには 「キーの世界の厳しいルール」 が関係しています。
JavaScript/TypeScript の仕様上、オブジェクトのキーとして使える値は string、number、symbol の3種類だけに限られています。boolean や object など、その他の型はこの世界に入ることができません。

つまり、キーの世界における「全体集合(宇宙)」は、unknown(なんでもあり)ではなく、「キーになりうる値のすべて」である string | number | symbol に制限されるのです。

ここまできて、ようやく謎の正体が掴めました。

型の世界で「始点(底)」にいた never は、世界が反転したことで、キーの世界の「終点(頂点)」へと持ち上げられました。そして、キーの世界の頂点とは、仕様上許される 「全てのキーの集合(string | number | symbol)」 のことに他なりません。

「もっとも狭い型(never)」のキーが、「もっとも広いキー(string | number | symbol)」になる。
それは直感に反するパラドックスではなく、「始点」が「終点」に入れ替わった結果として現れる、あまりにも自然な姿だったのです。

結論

keyof never が string | number | symbol になるのは、決してバグでも適当な仕様でもありません。

これまでの話をまとめると、以下のような論理的必然性が見えてきます。

  1. 反変性(矢印の反転):keyof を通すと、世界の関係性は逆転します。具体的な型(狭い型)ほど、キーの情報量は増え(広い型になり)ます(例:Dog のキーは Animal より多い)。
  2. 構造上の必然(底から頂点へ):
    この反転ルールに従えば、型の世界の「底」である never は、必然的にキーの世界の「頂点」(string | number | symbol)にならざるを得ません。

「集合の包含関係(ベン図)」という直感を捨て、「矢印の向き」という新しい地図を手に入れた今、この一見奇妙な挙動も、実は非常に美しい対称性を持った 「正しい答え」 であることが理解できたのではないでしょうか。

もし、この「矢印で考える面白さ」に少しでも心を動かされたのなら、ぜひ圏論の世界をもう少し覗いてみてください。そこには、普段書いているコードの景色を一変させるような美しい法則が、まだまだたくさん眠っています。


補足:少し厳密な話(圏論を知る人へ)

ここまでは直感的な理解を優先してきましたが、最後に少しだけ数学的に正確な補足をしておきます。

1. 恒等射の存在

本文中では説明を簡略化するために「never に入ってくる矢印はない」と述べましたが、圏論において対象(オブジェクト)であるためには、 自分自身への矢印(恒等射:Identity Morphism) が存在する必要があります。
当然、never や unknown も例外ではありません。

id_{never}: never \to never

never は「始対象(Initial Object)」であるため、任意の対象 A に対して射 never \to A が(一意に)存在し、自分自身への射も存在するため、圏論の要件をしっかり満たしています。

2. keyof は反変関手である

今回の解説で登場した「型の世界」と「キーの世界(部分圏)」の対応関係は、圏論では 反変関手(Contravariant Functor) として記述できます。

型の圏を \mathcal{T} 、その部分圏であるキーの圏を \mathcal{K} とすると、keyof は以下のような関手 F と見なせます。

F: \mathcal{T}^{op} \to \mathcal{K}

この関手 F(すなわち keyof)は、以下の2つの仕事を同時に行っています。

  1. 対象の変換: 型 T を、そのキーの型 Key(T) に写す。
  2. 射の反転: 型の包含関係 A \to B を、キーの包含関係 Key(B) \to Key(A) に写す。

私たちが「直感と逆だ」と感じた現象の正体は、この関手が持つ「反変性(Contravariance)」そのものだったのです。

気軽にEC『Lea = レア』

Discussion