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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?