この記事でわかること
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;
この型がやっていること
-
U extends any ? ... : never
で分配条件型を作る-
この書き方は「Uが
any
型に代入できるなら、(x: U) => void
型に変換し、そうでなければnever
にする」という意味です。 -
ほとんどの型は
any
に代入できるため、ここでは「Uの各要素に対して関数型を作る」という動作になります。 -
例えば
U = A | B
の場合、この部分だけで以下のようになります:(x: A) => void | (x: B) => void
-
ポイント:
extends
の左辺がユニオン型のとき、この条件式はユニオンの要素ごとに評価される(分配条件型の性質)。
-
-
関数の引数型を
infer
で推論する(x: infer I) => void
- 関数型のユニオンを1つの関数型に代入しようとすると、TypeScriptは引数型をインターセクション型として推論します。
これによりI
が{ a: string } & { b: number }
のように変わります。
- 関数型のユニオンを1つの関数型に代入しようとすると、TypeScriptは引数型をインターセクション型として推論します。
-
最終的にインターセクション型を返す
- 上記の推論に成功した場合は
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
部分で「条件を満たさない型は消す」ことができる。 -
void
やunknown
ではこの動作が変わってしまうため、削除目的なら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; }
このままだと、意図しない引数パターン(例: null
や boolean
)でも型のエラーとしては拾えません。
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
を意識すると、レビュー効率や保守性の向上にも直結します。