TypeScript の unknown、any、never:どれを使うか
1. 概要
TypeScript には混同しやすい 3 つの特殊な型があります。any、unknown、never です。この 3 つはすべて型システムの「端」に現れます。any は型チェックをオフにし、unknown は使用前に確認を強制し、never は起こり得ない場合を表します。
この記事を読み終わると、出所が不明なデータにどの型を選ぶべきかわかります。また、never を使ってコンパイラに処理忘れの分岐を報告させる方法も学べます。
すべての例は TypeScript 5.x と tsconfig.json の "strict": true で動作します。
2. any:型チェッカーをオフにする
any は値がなんでもできることを意味します。コンパイラはなにも確認しません:
let payload: any = "こんにちは";
payload.toUpperCase(); // OK
payload.foo.bar(); // コンパイル時 OK、実行時エラー
payload = 42;
const total: number = payload * 2; // OK
3 行目は通常にコンパイルされます。実行時には次のエラーが発生します:
TypeError: Cannot read properties of undefined (reading 'bar')
any では、TypeScript はあなたが責任を取ったと見なします。自動補完、プロパティ名チェック、安全なリファクタリングの利点をすべて失います。
2.1. any が伝播する
any の最大の問題は、それに触れるすべてに伝播することです:
function loadConfig(): any {
return JSON.parse('{"port": "8080"}');
}
const config = loadConfig();
const port = config.port; // port: any
const next = port + 1; // next: any
console.log(next);
80801
port は文字列 "8080" なので、port + 1 は 8081 ではなく "80801" になります。コンパイラは警告しません。any が config → port → next を通じて伝播しているからです。最初の any 1 つが関数呼び出しのチェーン全体の型チェックを失わせることができます。
注意:
JSON.parse、fetch API のresponse.json()、多くの古いライブラリはanyを返します。これが実際のコードで最も一般的なanyの流出源です。
3. unknown:型が不明な時の安全な選択肢
unknown も any と同じようにすべての値を受け入れます。違いは、型を絞り込む (narrowing) まで、その値を使用できないということです:
let payload: unknown = "こんにちは";
payload.toUpperCase();
error TS18046: 'payload' is of type 'unknown'.
コンパイラが即座にブロックします。使用前に payload が文字列であることを証明する必要があります:
if (typeof payload === "string") {
console.log(payload.toUpperCase()); // payload: string
}
こんにちは
if ブロック内では、TypeScript は payload が string であることを知っているため、toUpperCase() は有効です。
3.1. unknown を絞り込む方法
主に 4 つのツールがあります:
class PaymentError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
}
}
function describe(value: unknown): string {
if (typeof value === "number") {
return `数値:${value.toFixed(2)}`;
}
if (value instanceof PaymentError) {
return `支払いエラー ${value.code}`;
}
if (typeof value === "object" && value !== null && "id" in value) {
return `id を持つオブジェクト:${String(value.id)}`;
}
return "不明";
}
console.log(describe(12.5));
console.log(describe(new PaymentError("残高不足", "INSUFFICIENT")));
console.log(describe({ id: 7 }));
console.log(describe(null));
数値:12.50
支払いエラー INSUFFICIENT
id を持つオブジェクト:7
不明
typeofはプリミティブ型に使います:string、number、booleanなど。instanceofはクラス用です。inはプロパティの存在を確認します。TypeScript 4.9 以降、"id" in valueの後、valueはobject & Record<"id", unknown>型になります。typeof null === "object"なので、value !== nullを確認する必要があります。
3.2. 外部データ用の型ガード
複雑なオブジェクトの場合、型ガード(value is T を返す関数)を書くべきです:
interface Order {
id: string;
customerName: string;
total: number;
}
export function isOrder(value: unknown): value is Order {
if (typeof value !== "object" || value === null) return false;
const v = value as Record<string, unknown>;
return (
typeof v.id === "string" &&
typeof v.customerName === "string" &&
typeof v.total === "number"
);
}
export function parseOrder(json: string): Order {
const data: unknown = JSON.parse(json);
if (!isOrder(data)) {
throw new Error("注文データが無効です");
}
return data; // data: Order
}
console.log(parseOrder('{"id":"A1","customerName":"太郎","total":250000}'));
{ id: 'A1', customerName: '太郎', total: 250000 }
重要なポイントは const data: unknown = JSON.parse(json) という行です。型を明示的に unknown として割り当てることで、JSON.parse からの any が外へ伝播するのをブロックします。ここからは、コンパイラは使用前に data の確認を強制します。
大規模なプロジェクトでは、手書きの型ガードを Zod などのスキーマ検証ライブラリに置き換えることができます。原則は同じです:入力は unknown、検証後は具体的な型になります。
3.3. catch での unknown
strict: true(特に useUnknownInCatchVariables フラグ)では、catch 変数は unknown 型になります:
try {
parseOrder("{}");
} catch (err) {
// err: unknown
const message = err instanceof Error ? err.message : String(err);
console.log(message);
}
注文データが無効です
これは現実を反映しています。JavaScript ではなんでも throw できます(文字列や数値も)。err が常に Error であると仮定することはできません。
4. never:値を持たない型
never は空の型です。never に属する値は存在しません。これは主に 3 つの場面で現れます。
4.1. 戻らない関数
常にエラーをスローするか、無限ループを実行する関数は never を返します:
function fail(message: string): never {
throw new Error(message);
}
function getDiscount(code: string): number {
if (code === "TET2026") return 0.2;
return fail(`割引コードが存在しません:${code}`);
}
never はすべての型に割り当てられるため、return fail(...) は number を返す関数で有効です。コンパイラはまた、fail() の呼び出し後のコードに到達できないことを理解しています。
4.2. Exhaustive チェック
これは never の最も実用的な使い方です。支払い方法の union があるとします:
type PaymentMethod =
| { kind: "card"; last4: string }
| { kind: "cash" }
| { kind: "momo"; phone: string };
export function assertNever(value: never): never {
throw new Error(`未処理のケース:${JSON.stringify(value)}`);
}
export function paymentLabel(method: PaymentMethod): string {
switch (method.kind) {
case "card":
return `カード **** ${method.last4}`;
case "cash":
return "現金";
case "momo":
return `MoMo ${method.phone}`;
default:
return assertNever(method);
}
}
3 つの case をすべて処理した後、default では method は never に絞り込まれます。assertNever(method) の呼び出しは有効です。
今、同僚が { kind: "zalopay"; phone: string } を union に追加しても switch を修正し忘れたとします。コンパイラは即座に報告します:
error TS2345: Argument of type '{ kind: "zalopay"; phone: string; }' is not assignable to parameter of type 'never'.
エラーはコンパイル時に検出されます。ユーザーの画面に undefined ラベルが表示されるのを防ぎます。万が一、古い API からおかしなデータが実行時に入ってきた場合、assertNever は明確なエラーをスローします。
4.3. Conditional type での never
Conditional type では、A | never が A であるため、never を使ってメンバーを union から除外します:
type OnlyStrings<T> = T extends string ? T : never;
type Mixed = "vnd" | "usd" | 404 | null;
type Currencies = OnlyStrings<Mixed>; // "vnd" | "usd"
TypeScript は conditional type を union の各メンバーに配布します。404 と null は never になり、消えます。Exclude、Extract、NonNullable などの組み込みユーティリティ型もこの仕組みで動作します。
5. 比較表
コアの違いは割り当て可能性にあります:
| 値をこの型に割り当てる | この型を他の型に割り当てる | 直接使う(メソッド呼び出し、プロパティアクセス) | |
|---|---|---|---|
any |
可能 | 可能(never 除く) |
可能、チェックなし |
unknown |
可能 | unknown、any のみ |
不可、絞り込み必須 |
never |
不可(never 除く) |
可能、すべての型へ | 値が存在しない |
簡潔にまとめると:
unknownはトップ型で安全です。すべてがunknownです。neverはボトム型です。neverはすべての型の部分型です。anyは抜け道です。トップ型とボトム型の両方の役割を果たし、型システムの完全性を破壊します。
6. 実行動作のテスト
Vitest を使ってコンパイル時と実行時の両方の動作をテストできます。インストール:
npm install -D vitest typescript
セクション 3.2 と 4.2 の関数が order.ts と payment.ts にあるとします:
// payment.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { isOrder, parseOrder } from "./order";
import { paymentLabel, assertNever } from "./payment";
describe("unknown と型ガード", () => {
it("有効な注文を受け入れる", () => {
const order = parseOrder('{"id":"A1","customerName":"太郎","total":1000}');
expect(order.total).toBe(1000);
expectTypeOf(order.total).toEqualTypeOf<number>();
});
it("間違った型のデータを拒否する", () => {
expect(isOrder({ id: "A1", customerName: "太郎", total: "1000" })).toBe(false);
expect(() => parseOrder("null")).toThrow("注文データが無効です");
});
});
describe("never と exhaustive チェック", () => {
it("各支払い方法に正しいラベルを返す", () => {
expect(paymentLabel({ kind: "card", last4: "4242" })).toBe("カード **** 4242");
expect(paymentLabel({ kind: "cash" })).toBe("現金");
});
it("実行時に union の外のデータでエラーをスロー", () => {
const legacy = { kind: "paypal" } as unknown as never;
expect(() => assertNever(legacy)).toThrow("未処理のケース");
});
});
npx vitest run
✓ payment.test.ts (4 tests)
Test Files 1 passed (1)
Tests 4 passed (4)
expectTypeOf はコンパイル時に型をチェックします。何かの変更で order.total が any になった場合、vitest --typecheck を実行するとテストが失敗します。最後のテストは古いシステムから「おかしな」データが来ることをシミュレートして、実行時のセーフティネットが機能していることを確認します。
7. よくあるエラーと使うべきでない時
エラーを「かわすため」に any を使う。 これは最悪の習慣です。型が本当にわからなければ、unknown を使ってください。@typescript-eslint/no-explicit-any ルールを有効にしてこれらを検出してください。
any の代わりに as で unknown をキャストする。 data as Order はコンパイラをなだめるだけで、実行時には何も確認しません。本質的には any と同じくらい危険です。
const data: unknown = JSON.parse('{"id": 1}');
const order = data as Order; // コンパイル OK
order.customerName.trim(); // 実行時 TypeError
すべてに unknown を使う。 unknown はシステムの境界用です。ユーザー入力、API レスポンス、catch、デシリアライズされたデータ。ドメイン内では具体的な型を使ってください。calculateTotal(items: unknown) という関数は設計が悪いです。
never と void を混同する。 void は関数が戻るが有用な値がないという意味です。never は関数が決して戻らないという意味です。普通の log() 関数は void であり、never ではありません。
switch で default を忘れる。 assertNever がなければ、TypeScript は戻り値の型が明示的であり noImplicitReturns が有効な場合のみエラーを報告します。その場合でも、実行時のセーフティネットを失います。
注意:
anyにも正当な用途があります。大規模な JavaScript コードベースを段階的にマイグレーションする場合、または型が本当に動的なライブラリの宣言を書く場合です。それを非常に限定して、理由をコメントで説明してください。
8. 結論
この 3 つの型には明確な役割があります:
unknownを使う:システムの境界で型が不明なデータの場合。typeof、instanceof、in、または型ガードで絞り込みます。neverを使う:常にエラーをスローする関数、assertNeverでの exhaustive チェック、conditional type で union をフィルタリングする場合。anyを避ける:特別な理由があり、限定されている場合を除きます。
実際のルールはシンプルです。any と書きたくなったら、まず unknown を試してください。コンパイラがエラーを報告したら、それはあなたがデータを確認する必要がある場所です。