【TypeScript】any型・unknown型・never型の違い

JavaScript

こんにちは、かつコーチです。

前回は関数の型について解説し、最後に「any型に逃げると型チェックの恩恵を受けられなくなる」という話をしました。

今回はその続きとして、TypeScript特有の3つの型、anyunknownnever の違いをまとめて整理します。

見た目は似ていますが役割はまったく違うので、それぞれの使いどころをしっかり理解しておきましょう。

any型:型チェックを放棄する型

any型とは何か

anyは、「どんな型の値でも受け入れる」という特殊な型です。

let value: any = 10;
value = "文字列に変更"; // OK
value = { key: "オブジェクトにも変更" }; // OK

value.foo.bar.baz; // 存在しないプロパティでもコンパイルエラーにならない

any を使うと、TypeScriptはその変数に対する型チェックを一切行わなくなります。

存在しないプロパティにアクセスしても、コンパイル時にはエラーが出ません。

これは裏を返すと、TypeScriptを導入している意味がその変数に関しては失われてしまう、ということです。

any型が許される場面

とはいえ any がまったく不要というわけではありません。

外部ライブラリの型定義が存在しない場合や、段階的にJavaScriptからTypeScriptへ移行している途中のコードでは、一時的に any を使わざるを得ないこともあります。

ただし、これはあくまで「例外的な逃げ道」であり、恒常的に使う型ではないと考えておくのが安全です。

unknown型:安全な「なんでも受け入れる」型

unknown型とは何か

unknownany と同じく「どんな型の値でも代入できる」型です。

違いは、代入した後に使う際の扱われ方です。

let value: unknown = 10;
value = "文字列に変更"; // OK
value = { key: "オブジェクトにも変更" }; // OK

value.foo.bar.baz; // エラー:unknown型のままではプロパティにアクセスできない

unknown は代入は自由ですが、そのままの状態でプロパティにアクセスしたり、関数として呼び出したりすることはできません。

「中身が何であるか分からない値」を安全に扱うために、まず型を絞り込む作業が必要になります。

型を絞り込んでから使う

unknown 型の値を使うには、typeof などで型を確認してから使います。

function printLength(value: unknown): void {
  if (typeof value === "string") {
    console.log(value.length); // ここではvalueがstring型だと確定している
  } else {
    console.log("文字列ではありません");
  }
}

printLength("かつコーチ"); // 5
printLength(123); // "文字列ではありません"

typeof value === "string" のチェックを通過したブロックの中では、TypeScriptが valuestring 型だと自動的に絞り込んでくれます。

これを「型ガード」と呼びます。

any と違って、unknown は「使う前に必ず型を確認させる」という強制力があるため、安全に「型が不明な値」を扱える点が大きなメリットです。

any型とunknown型の使い分け

外部から受け取る値(APIレスポンスやユーザー入力など)で、まだ型が分からない場合は、基本的に unknown を使うのがおすすめです。

any は本当にどうしても型チェックを外したい特殊な場合だけに限定して使うようにしましょう。

never型:絶対に到達しない型

never型とは何か

neverは、これまでの二つとはまったく逆の性質を持つ型です。

「絶対に発生しない値」を表す型で、代入できる値がありません。

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

この関数は必ずエラーを投げて終了するため、正常に値を返すことがありません。

このような「関数が正常に終了しない」ことを表現するために never を戻り値の型として使います。

switch文の網羅性チェックで使う

never のもう一つの代表的な使い道が、switch 文などでの「網羅性チェック」です。

type Fruit = "apple" | "banana" | "orange";

function getColor(fruit: Fruit): string {
  switch (fruit) {
    case "apple":
      return "赤";
    case "banana":
      return "黄";
    case "orange":
      return "オレンジ";
    default: {
      const exhaustiveCheck: never = fruit; // すべてのcaseを網羅していれば、fruitはnever型になる
      throw new Error(`未対応の果物です: ${exhaustiveCheck}`);
    }
  }
}

Fruit 型の値がすべての case で処理されていれば、default に到達する頃には fruit の型は never になっているはずです。

もし将来 Fruit 型に新しい値(例えば "grape")が追加され、case の追加を忘れた場合、default 内の fruitnever ではなくなるため、const exhaustiveCheck: never = fruit; の行がコンパイルエラーになります。

つまり never を使うと、「型を追加したのに分岐の追加を忘れる」というミスをコンパイル時に検出できるようになります。

つまずきやすいポイント:unknown型を軽視して痛い目にあった話

実際につまずいた話

私が初めてAPIから返ってくるJSONデータを扱ったとき、レスポンスの型を any で受け取っていました。

その後の処理で response.data.user.name のようにネストしたプロパティに直接アクセスしていたのですが、APIの仕様変更で data の中身が空になるケースがあることに気づかず、本番環境で Cannot read properties of undefined というエラーが発生してしまいました。

any を使っていたせいで、TypeScriptはこの問題をまったく指摘してくれませんでした。

後から unknown 型に変更し、型ガードでチェックを入れるようにしたところ、コンパイルの段階で「data が存在するかどうか確認していません」というエラーが出るようになり、事前に対策を入れられるようになりました。

「外部から来るデータは信用できない」という前提に立って、any ではなく unknown を使うべきだったと痛感した経験です。

Before/Afterで確認する

❌ Before:any型で外部データをそのまま扱う

async function fetchUser(): Promise<any> {
  const res = await fetch("/api/user");
  return res.json();
}

async function showUserName(): Promise<void> {
  const user = await fetchUser();
  console.log(user.profile.name); // profileが存在しなくても実行時までエラーに気づけない
}

fetchUser の戻り値が any になっているため、user.profile.name のような存在しないかもしれないプロパティへのアクセスも、コンパイル時には一切チェックされません。

✅ After:unknown型で受け取り、型ガードで安全に絞り込む

type User = {
  profile: {
    name: string;
  };
};

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "profile" in value
  );
}

async function fetchUser(): Promise<unknown> {
  const res = await fetch("/api/user");
  return res.json();
}

async function showUserName(): Promise<void> {
  const data = await fetchUser();
  if (isUser(data)) {
    console.log(data.profile.name); // isUserを通過しているのでプロパティの存在が保証される
  } else {
    console.log("ユーザー情報の形式が想定と異なります");
  }
}

fetchUser の戻り値を unknown にし、isUser という型ガード関数で中身を確認してから使うようにすることで、想定外の形のデータが来た場合にも安全に処理を分岐できます。

value is User という戻り値の書き方は「型述語」と呼ばれ、この関数が true を返したときに valueUser 型として扱ってよい、とTypeScriptに伝える書き方です。

まとめ

この記事のポイント

  • any型はあらゆる型チェックを放棄する型で、多用するとTypeScriptの恩恵が失われる
  • unknown型は代入は自由だが、使う前に型を絞り込む必要がある安全な型
  • 外部から来る型が不明な値には、anyではなくunknownを使うべき
  • never型は「絶対に発生しない値」を表し、switch文の網羅性チェックなどに活用できる
  • 型ガード関数(value is User)を使うと、unknown型を安全に具体的な型へ絞り込める

次に読むべき記事

特殊な型の違いが分かったら、次は複数の型を組み合わせる union型とintersection型を学んでいきましょう。

→ 次の記事:union型(|)とintersection型(&)の使い方

タイトルとURLをコピーしました