TypeScriptのanyとunknownの違いは、anyは「型チェックをしない型」、unknownは「中身を確かめるまで使えない型」であることです。 どちらも何でも代入できますが、unknownは typeof などで型を絞り込むまでプロパティにアクセスできません。そのため、型がわからない値を受け取るときは、まずunknownを選ぶのが安全です。

この記事では、TypeScript 7.0.2(2026年9月時点のnpm最新版)で tsc を実行した実際のエラー文と、Node.js 22での実行結果を載せながら、unknownの絞り込み方(typeof・instanceof・in・型ガード関数・asserts)、catch句の e がunknownになる設定、JSON.parse の結果をzodで検証する方法、neverとの関係、既存コードのanyをunknownへ移す手順までを解説します。最後に「どちらを使うか」の判断表も用意しました。

anyとunknownの違いの早見表

比べる点 any unknown
何でも代入できるか できる できる
他の型の変数に代入できるか できる(チェックなし) できない(any と unknown にだけ代入可)
プロパティ・メソッドを使えるか 使える(存在しなくても通る) 絞り込むまで使えない
ミスに気づくタイミング 実行時 コンパイル時
型の位置づけ 型チェックの対象外 すべての型の上位(トップ型)
主な使いどころ 移行途中の一時的な逃げ道 外部入力、catch の e、JSON.parse の結果

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScriptのanyとunknownの違い|絞り込み方と使い分けの判断表」

検証環境

  • TypeScript 7.0.2(tsconfig.json で "strict": true。7.0では strict は既定でも true)
  • Node.js 22.22.2(型注釈を取り除いてそのまま .ts を実行)
  • zod 4.6.5
  • ESLint 10.11.0 / typescript-eslint 8.70.1(このときpeer依存の関係でTypeScript 6.0.3が入りました)

同じコードでanyとunknownを比べる

let a: any = "hello";
let u: unknown = "hello";
a.toFixed(2);         // コンパイルは通る
u.toFixed(2);
const s1: string = a; // any はどこにでも代入できる
const s2: string = u;
const x: unknown = a; // unknown には何でも代入できる

tsc の結果です。

src/c1.ts(4,1): error TS18046: 'u' is of type 'unknown'.
src/c1.ts(6,7): error TS2322: Type 'unknown' is not assignable to type 'string'.

unknownの行だけがエラーになり、anyの行は通りました。 ところが、anyの行をJavaScriptとして実行すると次のようになります。

TypeError: a.toFixed is not a function

anyは「間違っていても通す」、unknownは「確かめるまで通さない」と覚えると違いがはっきりします。

unknownの絞り込み方(型の絞り込み=ナローイング)

unknownの値は、条件分岐で型を確かめると、その分岐の中だけ具体的な型として扱えます。 これを「型の絞り込み(ナローイング)」と呼びます。

方法 確かめられるもの 例
typeof プリミティブ型(string, number, boolean, bigint, symbol, undefined, function, object) typeof v === "string"
instanceof クラスのインスタンス(Error, Date, 自作クラス) v instanceof Date
Array.isArray 配列 Array.isArray(v)
in オブジェクトが特定のキーを持つか "id" in v
型ガード関数 自分で決めた条件 function isUser(v): v is User
asserts 条件を満たさなければ例外 function assertUser(v): asserts v is User
スキーマ検証 複雑な構造をまとめて zodの safeParse

typeof・instanceof・in の組み合わせ

class HttpError extends Error {
  status: number;
  constructor(status: number, message: string) { super(message); this.status = status; }
}
function describe(v: unknown): string {
  if (typeof v === "string") return `string(${v.length})`;
  if (typeof v === "number") return `number(${v.toFixed(1)})`;
  if (v instanceof HttpError) return `HttpError(${v.status})`;
  if (v instanceof Date) return `Date(${v.toISOString()})`;
  if (Array.isArray(v)) return `array(${v.length})`;
  if (typeof v === "object" && v !== null && "id" in v) return `has id: ${String(v.id)}`;
  return "unknown";
}
for (const v of ["abc", 3.14159, new HttpError(404, "nf"), new Date(0), [1, 2], { id: 7 }, null]) {
  console.log(describe(v));
}

実行結果です。

string(3)
number(3.1)
HttpError(404)
Date(1970-01-01T00:00:00.000Z)
array(2)
has id: 7
unknown

各分岐の中では v.length や v.status がエラーなく使えています。 TypeScriptが条件から型を判断しているためです。

よくあるつまずき:typeof v === "object" だけでは足りない

function getId(v: unknown) {
  if (typeof v === "object") {
    return v.id;
  }
}
src/c7_bad.ts(3,12): error TS18047: 'v' is possibly 'null'.
src/c7_bad.ts(3,14): error TS2339: Property 'id' does not exist on type 'object'.

JavaScriptでは typeof null も "object" になるため、v !== null の確認が必要です。 さらに "id" in v でキーの存在を確かめると、v.id にアクセスできるようになります(前の例の最後の分岐)。

型ガード関数とasserts関数

同じ確認を何度も書くなら、戻り値の型を v is User にした「型ガード関数」にまとめます。例外で止めたい場合は asserts v is User を使います。

type User = { id: number; name: string };

function isUser(v: unknown): v is User {
  return (
    typeof v === "object" && v !== null &&
    "id" in v && typeof v.id === "number" &&
    "name" in v && typeof v.name === "string"
  );
}
function assertUser(v: unknown): asserts v is User {
  if (!isUser(v)) throw new TypeError("not a User");
}

const data: unknown = JSON.parse('{"id":1,"name":"Sato"}');
if (isUser(data)) console.log("guard:", data.name.toUpperCase());

const data2: unknown = JSON.parse('{"id":"1"}');
try {
  assertUser(data2);
  console.log(data2.name);   // この行以降、data2 は User として扱われる
} catch (e) {
  console.log("assert:", e instanceof Error ? e.message : e);
}
guard: SATO
assert: not a User
書き方 条件を満たさないとき 向いている場面
v is User(型ガード) false を返す if文で分岐したい
asserts v is User 例外を投げる 以降の処理を前提条件つきで書きたい

型ガード関数の中身が正しいかどうかはTypeScriptは確認しません。 条件を書き漏らすと、誤った型が付いてしまう点に注意します。構造が大きい場合は、次に紹介するzodのような検証ライブラリに任せると確実です。

catch句のeはunknown(useUnknownInCatchVariables)

strict: true のとき、catch (e) の e はunknownになります。 これは useUnknownInCatchVariables という設定で、strict に含まれています。JavaScriptでは throw "文字列" のようにError以外の値も投げられるためです。

try {
  JSON.parse("{broken");
} catch (e) {
  console.log(e.message);
}
src/c4_catch.ts(4,15): error TS18046: 'e' is of type 'unknown'.

--useUnknownInCatchVariables false を付けて同じファイルをチェックすると、このエラーは出ませんでした(e がanyになるため)。unknownのまま、Errorかどうかを確かめて使うのがおすすめです。

function toMessage(e: unknown): string {
  if (e instanceof Error) return e.message;
  if (typeof e === "string") return e;
  return JSON.stringify(e);
}
try {
  JSON.parse("{broken");
} catch (e) {
  console.log(toMessage(e));
}
try { throw "plain string"; } catch (e) { console.log(toMessage(e)); }
try { throw { code: 42 }; } catch (e) { console.log(toMessage(e)); }
Expected property name or '}' in JSON at position 1 (line 1 column 2)
plain string
{"code":42}

Errorでない値が投げられても、落ちずにメッセージを取り出せています。

JSON.parseの戻り値はanyなので、unknownで受けてzodで検証する

JSON.parse の戻り値の型は any です。 そのため、次のコードはコンパイルを通りますが、実行すると意図しない結果になります。

const cfg = JSON.parse('{"port":"8080"}');
const port: number = cfg.port;      // エラーにならない
console.log(port + 1);
80801

port は number と宣言されていますが、中身は文字列の "8080" だったため、足し算が文字列の連結になりました。 外部から来るデータ(APIレスポンス、設定ファイル、フォーム入力、localStorage)は、unknownで受け取って検証してから型をつけると安全です。

zodでの検証(zod 4.6.5で実行)

import * as z from "zod";

const User = z.object({
  id: z.number().int(),
  name: z.string().min(1),
  email: z.email().optional(),
});
type User = z.infer<typeof User>;   // スキーマから型を自動生成

function parseUser(json: string): User | null {
  const raw: unknown = JSON.parse(json);
  const result = User.safeParse(raw);
  if (!result.success) {
    console.log(z.prettifyError(result.error));
    return null;
  }
  return result.data;                 // ここでは User 型
}

console.log(parseUser('{"id":1,"name":"Sato"}'));
console.log(parseUser('{"id":"1","name":"","email":"x"}'));
{ id: 1, name: 'Sato' }
✖ Invalid input: expected number, received string
  → at id
✖ Too small: expected string to have >=1 characters
  → at name
✖ Invalid email address
  → at email
null

スキーマを1つ書くだけで、実行時の検証とTypeScriptの型が両方手に入ります。 型ガード関数を手で書くより漏れが起きにくく、エラー内容もどの項目が違うかまでわかります。なお、zod 4ではメールアドレスの検証は z.email() のようにトップレベルの関数で書けます。

unknown・any・neverの関係

unknownは「すべての型を含む一番上の型(トップ型)」、neverは「値が1つもない一番下の型(ボトム型)」です。 anyはこの階層の外にいて、型チェックそのものを行いません。

flowchart TD
    U["unknown(トップ型)<br>何でも入るが、そのままでは使えない"]
    U --> S[string]
    U --> N[number]
    U --> O["object など"]
    S --> NV["never(ボトム型)<br>値が存在しない"]
    N --> NV
    O --> NV
    A["any(階層の外)<br>型チェックをしない"]
式 結果 理由
unknown | string unknown 一番大きい型に吸収される
never | string string 空の型は消える
unknown & string string 共通部分は小さい方

neverで分岐の漏れを見つける

type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number } | { kind: "tri"; base: number; h: number };
function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.r ** 2;
    case "square": return s.size ** 2;
    default: {
      const unreachable: never = s;
      throw new Error(`unexpected: ${JSON.stringify(unreachable)}`);
    }
  }
}
src/c6_never.ts(7,13): error TS2322: Type '{ kind: "tri"; base: number; h: number; }' is not assignable to type 'never'.

"tri" の分岐を書き忘れたことを、コンパイル時に教えてくれます。 unknownが「何でも入る箱」なら、neverは「何も入らない箱」で、この性質を使うと網羅チェックができます。

anyからunknownへの移行手順

既存コードのanyは、一度に全部ではなく、影響の小さい場所から順にunknownへ置き換えると進めやすくなります。

手順 やること ポイント
1 strict: true を有効にする catch の e がunknownになり、noImplicitAny も有効になる
2 ESLintで no-explicit-any を有効にし、anyの場所を一覧にする まずは warn から始めてもよい
3 外部入力の境界(API・JSON・catch)をunknownにする ここが最も効果が大きい
4 unknownにした箇所を型ガードやzodで絞り込む エラーが出た場所が確認ポイント
5 内部の関数は具体的な型やジェネリクスに置き換える any[] → T[] など
6 残すanyには理由をコメントで書く // eslint-disable-next-line と理由をセットで

ESLintのno-explicit-anyを設定する

typescript-eslintの recommended 設定には、@typescript-eslint/no-explicit-any がエラーとして含まれています。 fixToUnknown: true を付けると、--fix でanyをunknownに書き換えられます。

// eslint.config.js
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";

export default defineConfig(
  tseslint.configs.recommended,
  {
    rules: {
      "@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
    },
  },
);
// src/sample.ts(修正前)
export function load(raw: any): any {
  return raw;
}

npx eslint src の結果です。

src/sample.ts
  1:27  error  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any
  1:33  error  Unexpected any. Specify a different type  @typescript-eslint/no-explicit-any

✖ 2 problems (2 errors, 0 warnings)
  2 errors and 0 warnings potentially fixable with the `--fix` option.

npx eslint src --fix の後のファイルです。

export function load(raw: unknown): unknown {
  return raw;
}

自動修正でunknownに変えたあと、tsc で出たエラーの場所が、絞り込みを追加すべき場所です。 自動修正は機械的な置き換えなので、変更後は必ず tsc とテストで確認します。

anyとunknownの使い分け判断表

場面 おすすめ 理由
APIレスポンス・JSON.parse の結果 unknown+検証 実データの形は実行時まで不明
catch (e) の e unknown Error以外も投げられる
何でも受け取ってログに出すだけ unknown 中身を使わないなら絞り込みも不要
配列や値の型を保ったまま処理 ジェネリクス T で型を引き継げる
型定義のない外部ライブラリ unknownで受けて型を宣言 使う部分だけ型を書く
JavaScriptからの移行途中 一時的にany 理由をコメントし、段階的に置き換え
テストのモック 状況に応じてany 範囲をテストファイル内に限定

迷ったら「unknownにして、使う直前に絞り込む」を基本にすると、型安全と書きやすさのバランスが取れます。

よくある質問

Q. unknownは毎回絞り込むのが面倒ではないですか? A. 絞り込みはデータの入口で1回行えば十分です。境界でzodや型ガード関数を通して具体的な型にすれば、それより内側のコードは普通の型として扱えます。

Q. as で型を付ければunknownでも使えますか? A. value as User と書けばコンパイルは通りますが、実行時の確認は行われません。 中身が違っていても気づけないため、外部データには型ガードや検証を使うのがおすすめです。

Q. object 型や {} 型とunknownは何が違いますか? A. object はプリミティブ以外、{} は null と undefined 以外の値を表します。null や undefined も含めて本当に何が来るかわからないときはunknownを使います。

Q. anyを使ってよい場面はありますか? A. JavaScriptから移行している途中や、型の表現がとても難しい一部の箇所など、一時的な逃げ道として使われます。範囲を小さくし、理由をコメントに残しておくと後から置き換えやすくなります。

まとめ

  • anyは型チェックをしない型、unknownは確かめるまで使えない型
  • unknownは typeof・instanceof・in・型ガード関数・asserts で絞り込む
  • typeof v === "object" の後は v !== null の確認も忘れずに
  • strict: true なら catch (e) の e はunknown。Errorかどうか確かめて使う
  • JSON.parse の結果はunknownで受けてzodで検証すると、型と実データがずれない
  • unknownはトップ型、neverはボトム型。neverで分岐の漏れを検出できる
  • 移行は no-explicit-any+fixToUnknown でanyを洗い出し、入口から順に置き換える

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「TypeScriptのanyとunknownの違い|絞り込み方と使い分けの判断表」

株式会社RJCでは、型を味方につけた安全なコードの書き方を、メンバー同士で学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社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で一緒に開発しながら、 成長を楽しみませんか?