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 文を更新し忘れると、lang は never ではなくなるため、代入エラーが発生してコンパイルが通らなくなる。これも 「起こり得ないはずの状態を 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)」 として扱われるんだ。
-
推論のプロセス:
const arr = []と書いたとき、TSは「将来何が入るか」を予測しようとするけれど、その行だけではヒントがゼロだよね。 -
型代数の結果:
「何も入っていない状態」を型で厳密に表現すると、それは「いかなる型の要素も持たない配列」になる。つまり、要素の型が空集合である
never[]に行き着くんだ。
2. (Type1 | Type2)[] との関係
君が言っていた「any[] らしい」というのは、文脈的におそらく 「何らかの配列を受け入れる準備がある場所」 という意味だね。
TypeScript
type MyData = {
items: (string | number)[];
};
const data: MyData = {
items: [] // ここで代入された [] 自体は、推論上は never[]
};
ここで重要なのは、never はすべての型のサブタイプである というルールだよ。
-
neverはstringにも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?