- TypeScript
TypeScript never型とは?網羅性チェックとvoidとの違い
TypeScriptのnever型とは、「値が1つも存在しない」ことを表す型です。 必ず例外を投げる関数の戻り値や、条件分岐で候補をすべて消したあとの変数に現れます。どんな値も代入できない性質を利用して、switch 文の分岐漏れをコンパイル時に見つける「網羅性チェック」に使うのが代表的な使い方です。
この記事では、網羅性チェックの書き方4通りに同じ変更(ユニオン型へケースを1つ追加)を加え、それぞれで出る tsc のエラー全文と実行時の動きを比べます。あわせて、never が現れる場面の早見表と、never・void・undefined・unknown の代入可否の表を、実際に型チェックした結果で示します。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / TypeScript 7.0.2(npx tsc --version の結果)。strict: true で確認しています。
never型の早見表
never が現れる場面
| 場面 | コード | 得られる型 |
|---|---|---|
| throw だけのアロー関数・関数式 | const fail = (m: string) => { throw new Error(m); } |
戻り値が never |
| throw だけの function 宣言 | function fail(m: string) { throw new Error(m); } |
戻り値は void(: never と書けば never) |
| 終わらないループ | const forever = () => { while (true) {} } |
戻り値が never |
| 絞り込みで候補がなくなった後 | string | number を typeof で両方処理した後の変数 |
never |
| 両立しない型の交差 | string & number、"a" & "b" |
never |
| 判別用プロパティが両立しない交差 | { kind: "a" } & { kind: "b" } |
never |
| Exclude で全部取り除いた | Exclude<"a" | "b", "a" | "b"> |
never |
| Extract で一致なし | Extract<"a" | "b", "c"> |
never |
| 空配列をオブジェクトのプロパティに書いた | { tags: [] } |
tags が never[] |
| キーのない型の keyof | keyof {} |
never |
| 必ず reject する Promise | Promise.reject(new Error("x")) |
Promise<never> |
never の性質
| 性質 | 内容 |
|---|---|
| never 型の変数に代入できる値 | なし(undefined や any も代入できない) |
| never 型の値を代入できる先 | すべての型 |
| ユニオンに混ぜたとき | 消える(string | never は string) |
| 交差に混ぜたとき | never になる(string & never は never) |
| void との違い | void は「戻り値を使わない」、never は「戻ってこない」 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript never型とは?網羅性チェックとvoidとの違い」
never が現れる理由を1つずつ確かめる
関数が最後まで到達しないとき
tsc --declaration で、推論された戻り値の型を出力しました。
// where.ts
export const fail = (message: string) => {
throw new Error(message);
};
export function failDecl(message: string) {
throw new Error(message);
}
export const forever = () => {
while (true) {}
};
export const rejected = Promise.reject(new Error("x"));
// 出力された where.d.ts
export declare const fail: (message: string) => never;
export declare function failDecl(message: string): void;
export declare const forever: () => never;
export declare const rejected: Promise<never>;
同じ「throw だけの関数」でも、アロー関数は never、function 宣言は void と推論されます。 function 宣言で never にしたいときは、戻り値に : never と明示します。
この違いは、呼び出したあとの絞り込みに影響します。
// push.ts(抜粋)
function failDecl(message: string) {
throw new Error(message);
}
function failNever(message: string): never {
throw new Error(message);
}
function f(v: string | undefined) {
if (v === undefined) failDecl("値がありません");
return v.length;
}
function g(v: string | undefined) {
if (v === undefined) failNever("値がありません");
return v.length;
}
push.ts(20,10): error TS18048: 'v' is possibly 'undefined'.
エラーになったのは f だけです。: never と書いた関数を呼ぶと、TypeScript は「その先には進まない」と判断して v を string に絞り込みます。
: never と書いた関数が最後まで到達できる場合は、次のエラーになります。
function stop(): never {
console.log("終わってしまう");
}
misc.ts(1,18): error TS2534: A function returning 'never' cannot have a reachable end point.
絞り込みで候補がなくなったとき
export function afterNarrowing(v: string | number) {
if (typeof v === "string") return "string" as const;
if (typeof v === "number") return "number" as const;
return v; // ここでの v は never
}
推論された戻り値は "number" | "string" でした。最後の return v は never 型なので、ユニオンから消えています。「候補を1つずつ消していき、残りがなくなると never になる」という動きが、あとで説明する網羅性チェックの土台です。
交差型と Exclude の結果
次のコードで、結果が never かどうかを判定しました。IsNever が true になる行だけがエラーなしで通ります。
// isnever.ts
type IsNever<T> = [T] extends [never] ? true : false;
const a: IsNever<string & number> = true;
const b: IsNever<{ kind: "a" } & { kind: "b" }> = true;
const c: IsNever<{ id: string } & { id: number }> = true;
const c2: IsNever<({ id: string } & { id: number })["id"]> = true;
const d: IsNever<Exclude<"a" | "b", "a" | "b">> = true;
const e: IsNever<Extract<"a" | "b", "c">> = true;
const f: IsNever<keyof {}> = true;
const g: IsNever<never | string> = true;
const h: IsNever<never & string> = true;
const m: IsNever<"a" & "b"> = true;
const n: IsNever<string & {}> = true;
isnever.ts(6,7): error TS2322: Type 'true' is not assignable to type 'false'.
isnever.ts(11,7): error TS2322: Type 'true' is not assignable to type 'false'.
isnever.ts(18,7): error TS2322: Type 'true' is not assignable to type 'false'.
(行番号は確認時のファイルのもので、順に c・g・n の行です。)
| 型 | never か | 補足 |
|---|---|---|
string & number |
never | 同時に満たす値がない |
{ kind: "a" } & { kind: "b" } |
never | リテラル型のプロパティが両立しない |
{ id: string } & { id: number } |
never ではない | オブジェクト型は残り、id プロパティが never になる |
Exclude<"a" | "b", "a" | "b"> |
never | すべて取り除かれた |
Extract<"a" | "b", "c"> |
never | 一致するものがない |
keyof {} |
never | キーがない |
never | string |
never ではない(string) |
ユニオンでは never が消える |
never & string |
never | 交差では never が勝つ |
string & {} |
never ではない | string として使える |
空配列が never[] になるとき
const config = { tags: [] };
config.tags.push("typescript");
push.ts(2,18): error TS2345: Argument of type '"typescript"' is not assignable to parameter of type 'never'.
要素の型を決める手がかりがないため、tags は never[] と推論されます。型を書けば解消します。
const tags: string[] = [];
tags.push("typescript");
const config: { tags: string[] } = { tags: [] };
config.tags.push("typescript");
なお、ローカル変数に const ids = []; と書いて push(1)、push("a") と続けた場合はエラーにならず、その後の型は (string | number)[] になりました。プロパティに書いた空配列とは扱いが異なります。
網羅性チェックの書き方4通りを比べる
網羅性チェックとは、ユニオン型のすべてのケースを処理したかどうかを、コンパイル時に確かめる書き方です。ケースをすべて処理すると default に来る値は never になるので、そこで「never であること」を要求しておきます。あとからケースが増えると never ではなくなり、エラーで気づけます。
比べるコード
Status が3種類の状態で4つの関数を書き、あとから "archived" を足します。
// exhaustive.ts
type Status = "todo" | "doing" | "done" | "archived"; // "archived" をあとから追加
// 1) never 型の変数に代入する
function label1(s: Status): string {
switch (s) {
case "todo":
return "未着手";
case "doing":
return "進行中";
case "done":
return "完了";
default: {
const _exhaustive: never = s;
return _exhaustive;
}
}
}
// 2) assertNever 関数に渡す
function assertNever(value: never): never {
throw new Error(`想定していない値です: ${JSON.stringify(value)}`);
}
function label2(s: Status): string {
switch (s) {
case "todo":
return "未着手";
case "doing":
return "進行中";
case "done":
return "完了";
default:
return assertNever(s);
}
}
// 3) satisfies never を付ける
function label3(s: Status): string {
switch (s) {
case "todo":
return "未着手";
case "doing":
return "進行中";
case "done":
return "完了";
default:
throw new Error(`想定していない値です: ${s satisfies never}`);
}
}
// 4) default で throw するだけ
function label4(s: Status): string {
switch (s) {
case "todo":
return "未着手";
case "doing":
return "進行中";
case "done":
return "完了";
default:
throw new Error(`想定していない値です: ${s}`);
}
}
// 5) 参考:default を書かず、戻り値の型に任せる
function label5(s: Status): string {
switch (s) {
case "todo":
return "未着手";
case "doing":
return "進行中";
case "done":
return "完了";
}
}
const input = "archived" as Status;
for (const f of [label1, label2, label3, label4]) {
try {
console.log(f.name, "→", f(input));
} catch (e) {
console.log(f.name, "→ 例外:", (e as Error).message);
}
}
ケースを1つ足したときの tsc のエラー全文
Status が3種類のときは、tsc はエラーなしで通りました。"archived" を足すと次のようになります。
$ npx tsc -p .
exhaustive.ts(13,13): error TS2322: Type '"archived"' is not assignable to type 'never'.
exhaustive.ts(33,26): error TS2345: Argument of type '"archived"' is not assignable to parameter of type 'never'.
exhaustive.ts(47,40): error TS1360: Type '"archived"' does not satisfy the expected type 'never'.
exhaustive.ts(66,29): error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
--locale ja を付けた場合の出力です。
exhaustive.ts(13,13): error TS2322: 型 '"archived"' を型 'never' に割り当てることはできません。
exhaustive.ts(33,26): error TS2345: 型 '"archived"' の引数を型 'never' のパラメーターに割り当てることはできません。
exhaustive.ts(47,40): error TS1360: 型 '"archived"' は想定された型 'never' を満たしていません。
exhaustive.ts(66,29): error TS2366: 関数に終了の return ステートメントがないため、戻り値の型には 'undefined' が含まれません。
4) の「default で throw するだけ」には、エラーが出ていません。 型の上では s が "archived" のまま文字列に埋め込まれるだけなので、TypeScript は問題と見なしません。
実行時の動き
型チェックを通さずに "archived" が渡ってきた場合の動きも確かめました(Node.js で直接実行)。
$ node exhaustive.ts
label1 → archived
label2 → 例外: 想定していない値です: "archived"
label3 → 例外: 想定していない値です: archived
label4 → 例外: 想定していない値です: archived
1) は例外を投げていないので、想定外の値をそのまま返しています。コンパイル時のチェックと実行時の守りは別物だとわかります。
4通りの比較表
| 書き方 | ケース追加時のエラー | エラーの示す位置 | 実行時に想定外の値が来たとき | 向いている場面 |
|---|---|---|---|---|
1) const _: never = s |
TS2322 | 変数名 | 自分で throw を書かない限り素通り | 関数を増やしたくないとき |
2) assertNever(s) |
TS2345 | 引数 s |
例外で止まる | 複数の switch で共通に使うとき |
3) s satisfies never |
TS1360 | s |
組み合わせた throw で止まる | 補助関数なしで1行で書きたいとき |
| 4) default で throw のみ | 出ない | — | 例外で止まる | 実行時の守りだけでよいとき |
| 5) default なし+戻り値の型 | TS2366 | 関数の戻り値の型 | undefined が返る |
全ケースで return する関数 |
補足です。
- 1) は
noUnusedLocalsを有効にしていると、変数を使わない書き方で TS6133 になります。変数名を_exhaustiveにして throw だけを続ける形で確かめたところ、次の出力でした。上のコードのように変数をreturnするか、2) か 3) を選ぶと避けられます。
unused.ts(9,13): error TS6133: '_exhaustive' is declared but its value is never read.
- 3) の
satisfiesは式の型を変えずに検査だけを行う演算子です。throw new Error(...)の中に入れておくと、型の検査と実行時の停止を1行で両立できます。 - 5) は「どのケースが足りないか」をメッセージに含みません。足りないケース名を知りたいときは 1)〜3) が向いています。
オブジェクトのユニオンでの完成形
判別用のプロパティ(ここでは kind)を持つオブジェクトのユニオンでも、同じ書き方が使えます。
// shape.ts
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function assertNever(value: never): never {
throw new Error(`想定していない値です: ${JSON.stringify(value)}`);
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}
console.log(area({ kind: "circle", radius: 1 }).toFixed(2));
console.log(area({ kind: "rect", width: 2, height: 3 }));
console.log(area({ kind: "triangle", base: 4, height: 3 }));
// 型の外から来た値(JSON など)を渡した場合
const fromJson = JSON.parse('{"kind":"hexagon","side":2}') as Shape;
try {
area(fromJson);
} catch (e) {
console.log((e as Error).message);
}
$ npx tsc -p . && node shape.ts
3.14
6
6
想定していない値です: {"kind":"hexagon","side":2}
case "triangle" の2行を消すと、残った型がそのままメッセージに出ます。
shape.ts(17,26): error TS2345: Argument of type '{ kind: "triangle"; base: number; height: number; }' is not assignable to parameter of type 'never'.
switch を使わない書き方:Record で対応表を作る
値を返すだけの分岐なら、対応表にする方法もあります。キーが足りないと TS2741 になります。
type Status = "todo" | "done";
const LABELS = { todo: "未着手" } satisfies Record<Status, string>;
unused.ts(21,32): error TS2741: Property 'done' is missing in type '{ todo: string; }' but required in type 'Record<Status, string>'.
never・void・undefined・unknown の違い
代入できるかどうかの表
const s = null! as 値の型; const t: 代入先の型 = s; という文を全組み合わせで作り、tsc でエラーになるかを調べました。○ は代入できる、× は TS2322 になった組み合わせです。
| 値の型 ↓ / 代入先 → | never | void | undefined | unknown | any | null | string |
|---|---|---|---|---|---|---|---|
| never | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
| void | × | ○ | × | ○ | ○ | × | × |
| undefined | × | ○ | ○ | ○ | ○ | × | × |
| unknown | × | × | × | ○ | ○ | × | × |
| any | × | ○ | ○ | ○ | ○ | ○ | ○ |
| null | × | × | × | ○ | ○ | ○ | × |
| string | × | × | × | ○ | ○ | × | ○ |
読み取れることは次の3点です。
- never の行はすべて ○:never 型の値は、どの型の変数にも代入できます。
- never の列は never 以外すべて ×:never 型の変数には、
anyもundefinedも代入できません。この性質が網羅性チェックを支えています。 - unknown は never の逆:unknown の列はすべて ○(何でも受け取れる)、unknown の行は unknown と any 以外 ×(そのままでは渡せない)です。
エラー文の例です。
mx.ts(15,39): error TS2322: Type 'undefined' is not assignable to type 'never'.
mx.ts(29,33): error TS2322: Type 'any' is not assignable to type 'never'.
mx.ts(10,34): error TS2322: Type 'void' is not assignable to type 'undefined'.
意味の違い
| 型 | 意味 | 値はあるか | 主な置き場所 |
|---|---|---|---|
| never | 戻ってこない/ありえない | ない | 必ず throw する関数の戻り値、網羅性チェック |
| void | 戻り値を使わない | 実行時は多くの場合 undefined |
戻り値のない関数の戻り値 |
| undefined | undefined という値そのもの |
ある(undefined の1つだけ) |
省略できるプロパティ、未設定の値 |
| unknown | 何が入っているかわからない | ある(何でも) | 外から受け取った値 |
void と never の違い
function log(message: string): void {
console.log(message);
} // 呼び出し元に戻る
function fail(message: string): never {
throw new Error(message);
} // 呼び出し元に戻らない
void の関数は処理を終えて呼び出し元に戻ります。never の関数は戻りません。 そのため、never の関数を呼んだあとのコードは到達しないものとして扱われます。allowUnreachableCode: false を指定して確かめた出力です。
function after(): number {
fail("ここで止まる");
return 1;
}
misc.ts(13,3): error TS7027: Unreachable code detected.
never は式の中でも使えます。?? の右側に置くと、値がないときにその場で止める書き方になります。
function first(items: string[]): string {
const item = items[0] ?? fail("空の配列です");
return item;
}
fail の戻り値が never なので、item の型は string になります(never はユニオンから消えるため)。
never でよく見るエラーと対応
| エラー | 出る場面 | 対応 |
|---|---|---|
TS2322 Type 'X' is not assignable to type 'never' |
網羅性チェックでケースが足りない/never 型の変数に代入した | 足りない case を追加する |
TS2345 Argument of type 'X' is not assignable to parameter of type 'never' |
assertNever に残りの型が渡った/never[] に push した |
case を追加する/配列に型を書く |
TS1360 Type 'X' does not satisfy the expected type 'never' |
satisfies never でケースが足りない |
足りない case を追加する |
TS2534 A function returning 'never' cannot have a reachable end point |
: never の関数が最後まで到達できる |
必ず throw するか、戻り値の型を見直す |
TS2366 Function lacks ending return statement... |
default のない switch でケースが足りない | case を追加する |
TS7027 Unreachable code detected |
never の関数を呼んだ後にコードがある(allowUnreachableCode: false) |
到達しないコードを整理する |
エラー文に出てくる 'X' の部分が、まだ処理していないケースです。そこを読めば、どの case を足せばよいかがわかります。
よくある質問
Q. never 型はどんなときに自分で書きますか?
A. 主に2つです。1つは必ず例外を投げる関数の戻り値(function fail(): never)、もう1つは網羅性チェック(const _: never = x、assertNever(x: never)、x satisfies never)です。それ以外の never は、交差型や Exclude の結果として TypeScript が導き出すものがほとんどです。
Q. default で throw しているのに、ケースの追加漏れに気づけないのはなぜですか?
A. throw new Error(...) だけでは、型に対して何も要求していないためです。この記事の確認でも、4) の書き方は tsc のエラーが出ませんでした。assertNever(s) や s satisfies never を加えると、コンパイル時に気づけるようになります。
Q. function 宣言で throw だけを書いたのに、戻り値が void になります。
A. TypeScript 7.0.2 での確認では、function 宣言は void、アロー関数と関数式は never と推論されました。function 宣言で never にしたいときは : never と明示します。明示すると、呼び出し後の絞り込みも効くようになります。
Q. 「parameter of type 'never'」が配列の push で出ます。
A. オブジェクトのプロパティに [] を書くと never[] と推論され、何も追加できない配列になります。{ tags: string[] } のように要素の型を書くと解消します。
まとめ
- never は「値が1つも存在しない」型。戻ってこない関数、候補がなくなった変数、両立しない交差型に現れる
- never 型の変数には何も代入できず、never 型の値は何にでも代入できる
- 網羅性チェックは、default に来た値が never であることを要求する書き方。ケース追加で TS2322・TS2345・TS1360 が出る
- default で throw するだけではコンパイル時のエラーは出ない。
assertNeverかsatisfies neverを組み合わせる - function 宣言の throw だけの関数は void と推論される。never にしたいときは明示する
- void は「戻り値を使わない」、never は「戻らない」。never の関数の後ろは到達しないコードになる
- エラー文の
'X'が、まだ処理していないケース
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript never型とは?網羅性チェックとvoidとの違い」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。