2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptのnever完全ガイド — 網羅性チェックから型設計テクまで

Posted at

この記事でわかること

  • never型の基本と特徴
  • neverを活用した10種類の実務的テクニック
  • 網羅性チェックだけではない型安全の広げ方

neverとは?

neverはTypeScriptにおける「値が存在し得ない型」です。
特徴は次の通りです。

  • 到達不能な状態を表す
  • ユニオン型から要素を除外できる
  • 実行時には存在しないためオーバーヘッドゼロ

💡 イメージ図

1. switchの網羅性チェック

function getSizeName(size: 's' | 'm' | 'l' | 'xl') {
  switch (size) {
    case 's': return 'small'
    case 'm': return 'medium'
    case 'l': return 'large'
    default:
      return size satisfies never; // 未対応ケースは型エラー
  }
}

ポイント

  • ユニオン型に新しい要素を追加しても、ここでエラーになるため実装漏れ防止
  • コンパイル時の型安全性を確保できるため、保守性が向上する

noFallthroughCasesInSwitchとの併用

TypeScriptのコンパイラオプションnoFallthroughCasesInSwitchを有効にすると、
**caseから次のcaseに意図せず処理が落ちる(フォールスルー)**場合もコンパイルエラーになります。

tsconfig.json例:

{
  "compilerOptions": {
    "noFallthroughCasesInSwitch": true
  }
}

コード例(エラー発生パターン):

switch (size) {
  case 's':
    console.log('small');
    // break; // ← これを忘れるとエラー
  case 'm':
    console.log('medium');
    break;
}

このオプションとneverの網羅性チェックを組み合わせることで、
caseの抜け漏れ意図しないフォールスルーの両方を防止できます。

2. オブジェクトマップのキー取りこぼし防止

type Size = 's' | 'm' | 'l' | 'xl';

const sizeLabel = {
  s: 'Small',
  m: 'Medium',
  l: 'Large',
  xl: 'Extra Large',
} satisfies Record<Size, string>;

ポイント

  • satisfies Record<Union, Type>でキーの欠落・余分なキーを検出
  • 翻訳テーブルや設定マップなどで有効

補足(neverとの関係)
この例ではneverを直接書いてはいませんが、もしキーが欠けていた場合、
型推論の内部ではその欠けたキーの型がneverとして扱われます。
これにより「そのキーは存在しない(値を持ち得ない)」という状態になり、
型エラーとして検出されます。

例:

const sizeLabel = {
  s: 'Small',
  m: 'Medium',
  l: 'Large',
} satisfies Record<Size, string>;
// ❌ エラー: プロパティ 'xl' が存在しない(内部的には never 型)

つまり、このパターンはneverによる安全性チェックが裏で働いています。

3. 到達不能アサートで制御フローを明確化

function fail(message: string): never {
  throw new Error(message);
}

function getUser(id?: string) {
  if (!id) fail('id is required');
  // 以降 id は string 型として推論される
}

ポイント

  • never戻り値で以降のコードからundefinedを排除
  • 不要なnullチェックを削減し可読性向上

4. mapped typesでプロパティを動的に削除

まず前提として、 mapped types(マップド型) は「既存の型をベースに、新しい型を作る」仕組みです。
配列のmapのように、キーや値を変形できます。

基本例:プロパティの型をすべて変更

type User = { id: number; name: string };

// すべてのプロパティを string に変換
type AllStrings<T> = {
  [K in keyof T]: string;
};

type UserStrings = AllStrings<User>;
// => { id: string; name: string }

[K in keyof T] の部分で「Tのすべてのキー」をループしています。

as 句を使ってキー名を変える

mapped types では as を使うと、キー名を変えたり消したりできます。

type RenameNameToFullName<T> = {
  [K in keyof T as K extends 'name' ? 'fullName' : K]: T[K]
};

type RenamedUser = RenameNameToFullName<User>;
// => { id: number; fullName: string }

neverを使ってキーを削除

キー名を never にすると、そのキーは型から消えます。
これが「プロパティを削除する」ための仕組みです。

type RemoveName<T> = {
  [K in keyof T as K extends 'name' ? never : K]: T[K]
};

type UserWithoutName = RemoveName<User>;
// => { id: number }

応用例:特定のキーを動的に削除

この仕組みを汎用化すると、任意のキーを動的に削除できる型が作れます。

type OmitByKey<T, U extends PropertyKey> = {
  [K in keyof T as K extends U ? never : K]: T[K]
};

type A = { id: string; secret: string; name: string };

// secretを削除
type PublicA = OmitByKey<A, 'secret'>;
// => { id: string; name: string }

ポイント

  • mapped types は「キーをループ処理する型」
  • as 句でキー名の変更や削除が可能
  • never を使うと削除が実現できる
  • Omit型の内部実装にもこの仕組みが応用されている

5. 無効な組み合わせを型で禁止

type FetchMode = 'json' | 'text';
type Options<M extends FetchMode> =
  M extends 'json' ? { parse: true } :
  M extends 'text' ? { encoding?: string } :
  never;

function fetchEx<M extends FetchMode>(url: string, mode: M, opts: Options<M>) {}

ポイント

  • 条件型でneverを返し、誤った引数パターンを型エラーに
  • API設計時にモードごとのパラメータ制限を表現

6. 型変換の土台として利用

never は、高度な型操作(型レベルプログラミング)の中でも中間的な変換ポイントとして頻繁に使われます。
特にユニオン型をインターセクション型に変換するテクニックでは必須の存在です。

ユニオン型をインターセクション型に変換する例

type UnionToIntersection<U> =
  (U extends any ? (x: U) => void : never) extends
  (x: infer I) => void ? I : never;

この型がやっていること

  1. U extends any ? ... : never で分配条件型を作る

    • この書き方は「Uがany型に代入できるなら、(x: U) => void型に変換し、そうでなければneverにする」という意味です。

    • ほとんどの型はanyに代入できるため、ここでは「Uの各要素に対して関数型を作る」という動作になります。

    • 例えば U = A | B の場合、この部分だけで以下のようになります:

      (x: A) => void | (x: B) => void
      
    • ポイント: extends の左辺がユニオン型のとき、この条件式はユニオンの要素ごとに評価される(分配条件型の性質)。

  2. 関数の引数型をinferで推論する

    (x: infer I) => void
    
    • 関数型のユニオンを1つの関数型に代入しようとすると、TypeScriptは引数型をインターセクション型として推論します。
      これにより I{ a: string } & { b: number } のように変わります。
  3. 最終的にインターセクション型を返す

    • 上記の推論に成功した場合はIを返し、そうでなければneverを返します。

実例

type A = { a: string };
type B = { b: number };

type ABUnion = A | B;
type ABIntersection = UnionToIntersection<ABUnion>;
// => { a: string } & { b: number }

なぜneverが必要なのか

  • neverは「存在しない型」を表すため、不適合なケースや削除したいケースを型から消せる。
  • 条件型の : never 部分で「条件を満たさない型は消す」ことができる。
  • voidunknown ではこの動作が変わってしまうため、削除目的ならneverが適している。

ポイント

  • U extends any ? ... : never? は「条件演算子(三項演算子)」であり、ここでは「Uがanyに代入できるか」を条件にしています。
  • この条件はほぼ常に成立するため、実質的には「ユニオン型の各要素に対して処理を適用するための分配トリガー」として使われます。
  • ライブラリ型定義(Reactの型や型操作ユーティリティなど)で頻出するパターンで、neverが削除の役割を担っています。

7. イベント種別×ペイロードの整合性保証

type Event =
  | { type: 'created', id: string }
  | { type: 'deleted', id: string }
  | { type: 'renamed', id: string, name: string };

function handle(e: Event) {
  switch (e.type) {
    case 'created':
    case 'deleted':
      console.log(e.id);
      return;
    case 'renamed':
      console.log(e.id, e.name);
      return;
    default:
      return e satisfies never;
  }
}

ポイント

  • 新イベント追加時に未対応ケースを検出
  • バックエンドとフロントのイベント定義同期に有効

8. ステートマシンで到達不可能な状態を防ぐ

ステートマシン(日本語では状態機械とも呼ばれる)とは、
システムやオブジェクトが特定の状態にあり、イベントや条件によって別の状態へ遷移するモデルのことです。
UI状態管理、業務フロー、通信プロトコルなど、様々な分野で使われます。

TypeScriptでは、この「状態」をユニオン型で表し、 存在し得ない状態(到達不可能な状態)neverで検出できます。

type Idle = { state: 'idle' }
type Loading = { state: 'loading' }
type Success = { state: 'success', data: string }
type Failure = { state: 'failure', error: Error }

type FetchState = Idle | Loading | Success | Failure;

function render(s: FetchState) {
  switch (s.state) {
    case 'success': return s.data;
    case 'failure': return s.error.message;
    case 'idle':
    case 'loading': return '...';
    default:
      return s satisfies never; // 到達不可能な状態を検出
  }
}

ポイント

  • ユニオン型に新しい状態を追加しても、未対応の状態があればコンパイル時にエラーになる
  • 実行時ではなく設計段階で不整合を防げる
  • ReduxやXStateなどの状態管理ライブラリとも相性が良い

9. 要素を追加できない配列

const empty: never[] = [];

ポイント

  • テスト・API初期値などで「空であること」を型で保証
  • 実務ではreadonly T[]の方が汎用的

10. 関数呼び出しの禁止パターン

オーバーロードの設計時に、到達不可能な呼び出しパターンを型で禁止したいことがあります。
その場合にneverを使うと、意図しない引数型をコンパイルエラーにできます。

function f(x: string): string;
function f(x: number): number;
function f(x: string | number) { return x; }

このままだと、意図しない引数パターン(例: nullboolean)でも型のエラーとしては拾えません。

neverを使って禁止する例

function f(x: string): string;
function f(x: number): number;
function f(x: never): never; // 到達不可能なパターンを追加
function f(x: string | number) {
  return x;
}

f('abc'); // OK
f(123);   // OK
f(true);  // ❌ Error: 'boolean' 型は never に割り当てられない

補足(neverとの関係)

  • never型は「値が存在しない型」なので、これを引数型にするとその呼び出しは不可能になります。
  • オーバーロードの中に never を入れると、「そのパターンにマッチする型は存在しない」ことを明示でき、意図しない呼び出しをコンパイル時に防止できます。
  • 実務では、サポートしていない型や将来追加予定のパターンを一時的に封じる用途にも使えます。

neverと似た型の違い

受け取れる値 主用途
never なし 到達不能・禁止の表現、網羅性チェック
void undefined(返り値位置) 返り値を使わない関数
unknown 何でも来るが未使用時は安全 入口の最大公約型(要ナローイング)
any 何でも来るし何でもできる 型安全を無視

まとめ

neverは型安全性を高める重要な道具です。
網羅性チェック、プロパティ除外、無効な組み合わせ防止など多用途に使えます。
特にnoFallthroughCasesInSwitchと組み合わせることで、caseの漏れと意図しないフォールスルーを同時に防げるため、現場でのバグ削減効果は大きいです。
また、2 のように直接neverを使わないケースでも、裏側でneverが型安全性を支えています。
設計段階からneverを意識すると、レビュー効率や保守性の向上にも直結します。

2
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?