Upgrade to Pro — share decks privately, control downloads, hide ads and more …

詳解 never 型 - ボトム型の性質を深掘り

Avatar for Nealle Nealle
October 31, 2026

詳解 never 型 - ボトム型の性質を深掘り

TSKaigi Sendai 2026 登壇資料
https://sendai.tskaigi.org/talks/15

never 型はすべての型のサブタイプとなるボトム型で、TypeScript には欠かせない重要な型の一つです。しかし、never を明示的に型注釈で書く機会は少ないこともあり、あまり深く性質について考える機会もないのではないでしょうか?

本発表では、never を空集合として捉え直すところを出発点とします。その視点から、Union、Intersection での挙動、Conditional Type の分配、標準ユーティリティ型の実装原理など基礎部分について説明します。その上で、網羅性チェック、Phantom Types による状態遷移の型強制、型レベル関数のシグネチャ制御など、never の活用ケースを例で示しながら体系的に紹介します。

Avatar for Nealle

Nealle

October 31, 2026

More Decks by Nealle

Other Decks in Technology

Transcript

  1. 自己紹介 氏名 立川 隆一 / Ryuichi Tatsukawa 所属 株式会社ニーリー プロダクト統括本部

    プラットフォーム本部 ArchitectureG 経歴 2016 - 2019(株式会社かなめ) Webアプリケーションエンジニアとしてキャリアをスタート 2019 - 2021(株式会社 Hmcomm) シニアエンジニアとして経験を積む 2021 - 2024(株式会社ビザスク) 一定期間エンジニアとして従事した後、チームリーダーとしてマネジメント業務を担当 2024 - (株式会社ニーリー) テックリードとしてジョイン 2
  2. 1|空集合としての never never = ボトム型 = 空集合 ∅ • •

    • すべての型の部分型 値の集合として見ると空集合 ∅ never 型の値は作れない unknown(すべての値) string number ∅ never(要素なし) object 5
  3. 1|空集合としての never never はどんな型にも代入できる declare const neverValue: never; const num:

    number = neverValue; // never は number のサブタイプ 👍 const str: string = neverValue; // never は string のサブタイプ 👍 const obj: object = neverValue; // object にも代入可能 👍 空集合 ∅ はどんな集合にも含まれる → never 型の値は、どの型の変数にも代入できる 6
  4. 1|空集合としての never never には何も代入できない const neverValue: never = 1; //

    ❌ 代入不可 never 型の変数には代入できない → never 型の値は作れない ※前ページは説明のためアンビエント宣言(declare)を使用 7
  5. 1|空集合としての never never は自動で現れる function narrowToNever(value: string | number) {

    if (typeof value === "string" && typeof value === "number") { // 到達できないため、 value は never 型になる value; } } ・throw だけの関数の戻り値 ・narrowing で全ケースを排除した後の変数 ・Conditional Type の分岐失敗 などなど 8
  6. 2|基礎 Union の単位元、 Intersection の吸収元 type UnionWithNever = string |

    never; // = string type IntersectionWithNever = string & never; // = never Union の単位元 Intersection の吸収元 T | never = T T & never = never 空集合と合わせても元のまま 空集合との共通部分は空集合 ※ 補足 ・単位元: 計算しても相手を変えない要素 ・吸収元: 計算すると相手を飲み込んで自分と同じにしてしまう要素 10
  7. 2|基礎 never に置き換える = Union から消す type Letters = "a"

    | "b" | "c"; type LettersWithoutB = "a" | never | "c"; // = 'a' | 'c' type OnlyA = "a" | never | never; // = 'a' この性質が Union フィルタリング と Mapped Types のキー消去 の肝 11
  8. 2|基礎 Conditional Type の分配 T extends U ? X :

    Y の左辺が型パラメータ T そのもののとき、 Union が渡されると、各要素に分配される type ToArray<T> = T extends any ? T[] : never; type DistributedArrays = ToArray<"a" | "b">; // = 'a'[] | 'b'[] ToArray<"a" | "b"> ToArray<"a"> | ToArray<"b"> 'a'[] | 'b'[] 12
  9. 2|基礎 never は「0 要素の Union」 type ToArray<T> = T extends

    any ? T[] : never; type DistributedNever = ToArray<never>; // = never never[] になりそうに見えるが、そうはならない 分配する対象がない → 結果も never 13
  10. 2|基礎 分配を抑制する [T] extends [U] type ToArrayNoDistribute<T> = [T] extends

    [any] ? T[] : never; type NonDistributedNever = ToArrayNoDistribute<never>; // = never[] [T] で包むと、extends の左辺は T そのものではなくなる → T に何が入っていても分配されない 14
  11. 2|基礎 ハマりどころ ❌ NG type IsNeverWrong<T> = T extends never

    ? true : false; type WrongResult = IsNeverWrong<never>; // = never (❌ true を期待していた ) // 分配する要素が一つもなく、 extends never の判定は一度も行われない 👍 分配を抑制する type IsNever<T> = [T] extends [never] ? true : false; type NeverResult = IsNever<never>; // = true 👍 type StringResult = IsNever<string>; // = false 👍 15
  12. 2|基礎 Exclude を実装 type Exclude<T, U> = T extends U

    ? never : T; type LettersWithoutB = Exclude<"a" | "b" | "c", "b">; // 分配して展開すると : // ('a' extends 'b' ? never : 'a') → 'a' // | ('b' extends 'b' ? never : 'b') → never // | ('c' extends 'b' ? never : 'c') → 'c' // = 'a' | never | 'c' = 'a' | 'c' 16
  13. 2|基礎 Extract / NonNullable も同じ原理で実装できる Extract:マッチするものだけを残す( Exclude の逆) type Extract<T,

    U> = T extends U ? T : never; NonNullable:null と undefined を never にして消す type NonNullable<T> = T extends null | undefined ? never : T; 17
  14. 2|基礎 Mapped Types でキーを消す キーを never にしたエントリは結果から消える type MyOmit<T, K

    extends keyof T> = { [P in keyof T as P extends K ? never : P]: T[P]; }; MyOmit<{ a: 1; b: 2; c: 3 }, "b"> { a: 1; c: 3 } キー b が never になる 18
  15. 3|活用例(網羅性チェック) 前提 type Shape = | { kind: "circle"; radius:

    number } | { kind: "square"; side: number } | { kind: "triangle"; base: number; height: number }; 判別可能なユニオン(Discriminated Union) の全ケースを処理したかを コンパイル時に検証したい 20
  16. 3|活用例(網羅性チェック) never 型の変数で受けてチェック function area(shape: Shape): number { switch (shape.kind)

    { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "triangle": return 0.5 * shape.base * shape.height; default: const _exhaustive: never = shape; // ← 全ケース消化なら shape は never throw new Error(`unreachable: ${JSON.stringify(_exhaustive)}`); } } 21
  17. 3|活用例(網羅性チェック) 引数の型を never にしたヘルパーを用意してチェック function assertNever(value: never): never { throw

    new Error(`unexpected value: ${JSON.stringify(value)}`); } function area(shape: Shape): number { switch (shape.kind) { // ...各 case default: return assertNever(shape); } } 22
  18. 3|活用例(網羅性チェック) satisfies never でチェック function area(shape: Shape): number { switch

    (shape.kind) { // ... 各 case default: shape satisfies never; throw new Error("unreachable"); } } case を書き忘れていると satisfies でエラーが発生する 23
  19. 3|活用例( Phantom Types) 前提 interface User { name: string; email:

    string; } declare function readForm(): Promise<User>; declare function validate(user: User): User; declare function save(user: User): Promise<void>; async function submitForm() { const formUser: User = await readForm(); await save(formUser); // ❌ validate を通したかったのに、型では止められない } validate の前後で型が同じ User なので、区別できない 例のように validate を忘れても気付けない 24
  20. 3|活用例( Phantom Types) 型パラメータだけでは不十分 interface User<State extends "unvalidated" | "validated">

    { readonly name: string; readonly email: string; // State はどこにも使われていない // → User<'unvalidated'> と User<'validated'> は同一型扱い } declare function save(user: User<"validated">): Promise<void>; const unvalidatedUser: User<"unvalidated"> = { name: "A", email: "a@b" }; save(unvalidatedUser); // ❌ 通ってしまう (本当は弾きたい ) 構造に現れない型パラメータは、構造的型システムでは区別されない 25
  21. 3|活用例( Phantom Types) never でブランドを付ける type Phantomic<Base, Brand extends string>

    = Base & { [Key in Brand]: never }; type ValidationState = "unvalidated" | "validated"; type RawUser = { readonly name: string; readonly email: string }; type User<State extends ValidationState> = Phantomic<RawUser, State>; // 展開: // User<'unvalidated'> = RawUser & { unvalidated: never } // User<'validated'> = RawUser & { validated: never } やっていることの本質としては Branded Types と変わらない never のフィールドが実行時に現れない(作れない)点がポイント 26
  22. 3|活用例( Phantom Types) 状態遷移を型で強制する const unvalidated = (raw: RawUser): User<"unvalidated">

    => raw as User<"unvalidated">; declare function validate(user: User<"unvalidated">): User<"validated">; declare function save(user: User<"validated">): Promise<void>; async function registerUser() { const user = unvalidated({ name: "Alice", email: "[email protected]" }); await save(user); // ❌ User<'unvalidated'> は save に渡せない await save(validate(user)); // 👍 } save 関数の引数が User<"validated"> 型でなければ実行できない 27
  23. 3|活用例(シグネチャ制御) 基礎: never 引数は呼び出せない function forbidden(arg: never): void {} forbidden("hi");

    // ❌ never には何も代入できない → 呼び出し自体が型エラー になる これを条件付き で発火させると、特定の呼び出しだけを禁止できる 28
  24. 3|活用例(シグネチャ制御) await の付け忘れを検出 type User = { name: string };

    declare function fetchUser(): Promise<User>; function saveToCache<T>(value: T extends Promise<unknown> ? never : T): void {} async function cacheCurrentUser() { saveToCache(await fetchUser()); // T = User → 引数の型は User 👍 saveToCache(fetchUser()); // ❌ } T が Promise のときだけ引数の型が never に変わる 29
  25. 3|活用例(シグネチャ制御) 基礎: prop?: never による XOR type UserQuery = |

    { id: string; email?: never } | { id?: never; email: string }; declare function findUser(query: UserQuery): void; findUser({ id: "u_1" }); // 👍 findUser({ email: "[email protected]" }); // 👍 findUser({ id: "u_1", email: "[email protected]" }); // ❌ どちらも指定は不可 findUser({}); // ❌ どちらかは指定しなければならない email?: never → 「email は省略してよいが、値を置くなら never(つまり値は置けない)」 30
  26. 3|活用例(シグネチャ制御) React の props で使用する例 type ButtonProps = { children:

    ReactNode } & ( | { href: string; onClick?: never } | { href?: never; onClick: () => void } ); <Button href="/settings">設定</Button> // 👍 <Button onClick={save}>保存</Button> // 👍 <Button href="/settings" onClick={save}>設定</Button> // ❌ どちらも指定は不可 <Button>???</Button> // ❌ どちらかは指定しなければならない 共通の props は外出し、排他にしたい部分だけ prop?: never の Union に 31
  27. 3|活用例(シグネチャ制御) React の props で使用する例 function Button(props: ButtonProps) { if

    (props.href !== undefined) { // props.href: string return <a href={props.href}>{props.children}</a>; } // props.onClick: () => void(undefined を含まない) return ( <button onClick={props.onClick}> {props.children} </button> ); } href の有無で Union が絞り込まれる 32
  28. まとめ 基礎 活用例 Union の単位元 / Intersection の吸収元 網羅性チェック:到達できない位置に never

    Conditional Type では 0 要素の Union Phantom Types:never でブランドを付ける Exclude / Omit は never に置き換えて消す シグネチャ制御:引数を条件付きで never に never = 空集合 ∅(すべての型の部分型) 34