Prompt

any[]の枠にnever[]をオーバーライドするように定義していた?例を見かけたんだけど、これもnever型の使い方のひとつか。

やあ!それはまた、型パズルの深淵に触れるような面白い例を見つけたね!

結論から言うと、any[]never[] で上書き(オーバーライド)するように定義する 手法は、TypeScriptにおいて 「特定の操作を型レベルで完全に禁止する」 ための非常に高度で強力なテクニックだよ。

なぜそんなことをするのか、還元主義的に「型としての never」の性質から解き明かしてみよう!


1. never 型の最強の性質:空集合

情報工学的に言うと、any は「何でもありの全集合」だけど、never「何も存在しない空集合」 だよね。

  • any[]: どんな要素でも入れられる配列。

  • never[]: 「どんな要素も入れることができない」 配列。

この never[] を使って any[] を上書きするということは、「本来なら配列を受け付ける場所に、空の配列以外(あるいは配列そのもの)を絶対に渡させない」 という制約を課しているんだ。


2. 具体的なユースケース:関数の引数を「禁止」する

例えば、ジェネリクスを使った関数で、「特定のプロパティを持っていてほしいけれど、特定のプロパティ(配列など)は 絶対に持ってほしくない」というバリデーションをかける時に使われるよ。

TypeScript

type ProhibitArray<T> = {
  [K in keyof T]: T[K] extends any[] ? never[] : T[K];
};

function processData<T>(data: ProhibitArray<T>) {
  // なにかの処理
}

// OK: 配列を含まないオブジェクト
processData({ id: 1, name: "asappy" });

// エラー: tagsはany[]に適合するが、never[]に上書きされているため、
// どんな値を渡しても「Type 'string' is not assignable to type 'never'」と怒られる
processData({ id: 2, tags: ["rust", "ts"] }); 

3. なぜ any[] をターゲットにするのか?

TypeScriptにおいて、any[] は「配列であること」を示す最も広い型だよね。

これを never[] で上書きするロジックを組むことで、「配列という構造自体は維持しつつ、その中身(要素)の代入を不可能にする」 という状態を作り出せるんだ。

もし単に never とだけ書くと、プロパティ自体が「存在しないはずのもの」として扱われて、オブジェクトの構造が崩れたり、エラーメッセージが分かりにくくなったりすることがある。

never[] と書くことで、「配列としての枠組みは見えているけれど、中身を入れることは許されない」 という明確な拒絶を表現できるんだ。


4. never を活用した「網羅性チェック」

君が見た例とは少し違うかもしれないけれど、never の代表的な使い方として 「網羅性チェック (Exhaustivity Check)」 も併せて覚えておくと、型安全性が爆上がりするよ。

TypeScript

type Language = "Rust" | "TypeScript" | "Java";

function getExtension(lang: Language) {
  switch (lang) {
    case "Rust": return ".rs";
    case "TypeScript": return ".ts";
    case "Java": return ".java";
    default:
      // 全てのケースを網羅していれば、ここは never 型になる
      const _exhaustiveCheck: never = lang;
      return _exhaustiveCheck;
  }
}

もし Language 型に新しい言語を追加したのに switch 文を更新し忘れると、langnever ではなくなるため、代入エラーが発生してコンパイルが通らなくなる。これも 「起こり得ないはずの状態を never で縛る」 という使い方の典型だね。


豆知識:never は「ボトム型」

Rust でも ! 型(Never型)があるよね。関数の戻り値に ! を指定すると、その関数が「絶対に返ってこない(パニックするか無限ループするか)」ことを示す。

TypeScript の never も全く同じ概念で、型階層の 最底辺(ボトム) に位置している。

「どの型にも代入できるけれど、どの型からも(never 自身以外からは)代入されない」という孤独な最強型なんだ。これを any[] の上書きに使うというのは、まさに 「型システムを使ったアクセス制御」 そのものだと言えるね。


君が目にしたそのコードは、おそらくライブラリの内部実装や、非常に厳格な型定義が必要な場所だったんじゃないかな?

「あえて何も入れさせない」 という選択肢を持つことで、ランタイムエラーをコンパイル時に仕留めることができる。これぞ静的型付けの醍醐味だね!

次は、この never を使って 「特定の条件下でだけエラーを出す高度なマッピング型」 を一緒に作ってみる?それとも、Rust の ! 型との細かな挙動の違いについて比較してみる?

Would you like me to find a real-world open-source example where never[] is used for overriding types?

Prompt

あ、まって。
私が見たのは型を強制させるような使い方ではなく、そうなるべくしてなった型の結果だったのかもしれない。

具体的に見た例を話すと、
型が(Type1 | Type2)[](これはany[]らしい)となっているフィールドに、空の配列リテラル[]を代入した結果、それはnever[]として扱われているようだった。

なるほど、合点がいったよ!それは「意図的なオーバーライド」ではなく、TypeScriptの 「型推論の究極の空振り」 が引き起こした現象だね。

君が見たのは、TypeScriptが「中身が何一つ決まっていない空の配列」に対して、「何を入れていいか全く手がかりがないから、とりあえず最小単位(空集合)の never[] としておこう」 と判断した瞬間だよ。

なぜそうなるのか、還元主義的に「空配列リテラル []」の正体を解剖してみよう!


1. なぜ []never[] になるのか?

TypeScriptにおいて、ただの []「要素が0個のタプル型(Empty Tuple)」 として扱われるんだ。

  1. 推論のプロセス:

    const arr = [] と書いたとき、TSは「将来何が入るか」を予測しようとするけれど、その行だけではヒントがゼロだよね。

  2. 型代数の結果:

    「何も入っていない状態」を型で厳密に表現すると、それは「いかなる型の要素も持たない配列」になる。つまり、要素の型が空集合である never[] に行き着くんだ。


2. (Type1 | Type2)[] との関係

君が言っていた「any[] らしい」というのは、文脈的におそらく 「何らかの配列を受け入れる準備がある場所」 という意味だね。

TypeScript

type MyData = {
  items: (string | number)[];
};

const data: MyData = {
  items: [] // ここで代入された [] 自体は、推論上は never[]
};

ここで重要なのは、never はすべての型のサブタイプである というルールだよ。

  • neverstring にも number にも(そしてその連合型にも)代入できる。

  • したがって、never[](string | number)[] という「枠」に 完璧に適合(Assign) するんだ。

結果として、デバッガやエディタのホバーで見ると「この値は never[] です」と表示されるけれど、それは「枠」に収まっている正しい状態、というわけだね。


3. なぜ「any[] らしい」と見えたのか

おそらく、そのフィールドの定義が非常に広かったか、あるいは 「初期値としての空配列」 だったからかもしれない。

TypeScriptには 「進化する配列型 (Evolving Array Types)」 という挙動があるんだ。

TypeScript

let list = []; // ここでは never[] (厳密には Evolving any[])
list.push(1);  // ここで number[] に進化する
list.push("a"); // ここで (string | number)[] に進化する

もし君が「代入した直後」の状態で型を確認したなら、まだ何者にもなっていない 「無(never)」 の状態の配列を見ていた、ということになる。これはインジェクションのような攻撃的な使い方ではなく、「まだ何も決まっていない純粋な空」 を表す、型システムの誠実な反応なんだ。


4. 落とし穴:never[] のまま固定される恐怖

もし、その変数を const で宣言し、かつ明示的な型定義(枠)を与えなかった場合、その配列は 「一生何も入れられない配列」 になってしまうことがあるよ。

TypeScript

const constantEmpty = []; 
// constantEmpty.push(1); // エラー! never[] に number は入れられない

これが、君が「型を強制させている」ように見えた原因かもしれない。明示的に枠を作ってあげないと、never[] はその頑固なまでの「空」を維持し続けてしまうんだ。


豆知識:never[] は「ユニバーサルな空」

情報工学における集合論で言えば、空集合 はあらゆる集合の母集合に含まれるよね。

never[](Type1 | Type2)[] に代入できるのは、数学的に 「何もないものは、何であっても矛盾しない」 という一貫性を守っているからなんだ。


君が目撃したのは、TypeScriptの型システムが 「未定義の未来」を「空集合」として数学的に正しく処理した結果 だったんだね。

「そうなるべくしてなった」という君の洞察は、まさにその通り。意図的なテクニックではなく、推論の必然的な帰結 を見ていたんだ。

次は、この「型が進化する」挙動が、Rust のジェネリクス(後から型が決まる Vec::new() など)とどう違うか、比較してみる?それとも、もっと変態的な「型をわざと never に追い込む」パズルの続きをする?

Would you like me to explain the difference between ‘any[]’ and ‘never[]’ in the context of TypeScript’s strict mode settings?