【TypeScript】TS18046の原因と解決方法|is of type ‘unknown’を完全解説

【TypeScript】TS18046の原因と解決方法|is of type 'unknown'を完全解説 TypeScript

TypeScriptで開発をしていると、ある日突然 'X' is of type 'unknown' というエラーに遭遇することがあります。このエラーの正式名称は TS18046 です。

TS18046は、unknown型の値をそのまま使おうとしたときに発生するエラーです。TypeScript 4.4以降で useUnknownInCatchVariablesstrict モードに含まれるようになったことで、catch句の変数が自動的にunknown型になり、このエラーに遭遇する開発者が急増しています。

unknown型は「型安全なany」とも呼ばれ、外部から入ってくるデータを安全に扱うためのTypeScriptの重要な仕組みです。しかし、unknown型の値は型を絞り込むまで一切の操作が許可されないため、正しい扱い方を知らないとコード全体が動かなくなります。

この記事では、TS18046エラーが発生する全パターンを網羅し、それぞれについてエラーメッセージNGコード原因の解説OKコード(修正例)を掲載します。try-catch、JSON.parse、APIレスポンス、ジェネリクス、React、バリデーションライブラリまで、実務で遭遇するあらゆるケースを完全にカバーします。

この記事で学べること

  • TS18046エラーメッセージの読み方意味
  • unknown型とは何か / any型との違い
  • try-catchでcatch変数がunknownになるケースと解決方法
  • JSON.parseの戻り値を型安全に扱うパターン
  • fetch API / axiosのレスポンスを安全に扱う方法
  • ジェネリクスとunknownの関係
  • typeof / instanceof / in / ユーザー定義型ガードによる型絞り込み完全ガイド
  • unknown を安全に扱うユーティリティ関数集
  • Zod / valibot / io-tsなどバリデーションライブラリとの連携
  • ReactでのTS18046(ErrorBoundary・useReducer等)
  • tsconfig.jsonの関連設定と段階的移行戦略

前提知識:この記事はTypeScriptの基本的な型の書き方を理解している方を対象としています。型の基本から学びたい方は【TypeScript】型の書き方 完全入門を先にお読みください。

スポンサーリンク
  1. TS18046エラーとは?
    1. エラーメッセージの読み方
    2. unknown型とは何か
    3. any型 vs unknown型の違い(コード比較)
    4. なぜunknownを使うべきなのか(型安全性のメリット)
    5. TypeScript 4.4以降でTS18046が増えた理由
    6. TS18046が発生する主なケース一覧
  2. 基本的なケースと解決方法
    1. unknown型の変数を直接使おうとした場合
    2. typeof による型の絞り込み
    3. instanceof による型の絞り込み
    4. 型アサーション(as)での解決
    5. 型ガード関数(ユーザー定義型ガード)での解決
    6. assertsキーワードによる型ガード
    7. 解決方法の比較まとめ
  3. try-catch の catch でのTS18046
    1. catch(e) の e が unknown になる理由
    2. 解決方法1: instanceof Error でチェック(推奨)
    3. 解決方法2: カスタムエラークラスの判定
    4. 解決方法3: エラーメッセージの安全な取得パターン
    5. 解決方法4: isError型ガードの自作
    6. 解決方法5: catch(e: any) で回避(非推奨)
    7. 実務でよく使うcatchパターン集
  4. JSON.parse でのTS18046
    1. JSON.parseの戻り値を安全に扱う
    2. Zod によるバリデーション(推奨)
    3. safeParse ユーティリティの自作
  5. API レスポンス・外部データでのTS18046
    1. fetch APIレスポンスの安全な型付け
    2. axiosレスポンスの安全な型付け
    3. localStorage / sessionStorage の値
    4. WebSocket メッセージの安全な型付け
  6. ジェネリクスと unknown のTS18046
    1. ジェネリクス関数の型引数がunknown
    2. Record<string, unknown> の扱い
    3. unknown[] 配列の操作
  7. 型の絞り込みテクニック(完全版)
    1. typeof ガード
    2. in 演算子ガード
    3. 等値チェック(=== / !==)
    4. Array.isArray() による配列判定
    5. Discriminated Union パターン
  8. unknown を安全に扱うユーティリティ関数集
    1. プリミティブ型ガード
    2. isError / toError
    3. hasProperty(プロパティ存在チェック)
    4. narrowType(汎用型絞り込み)
  9. バリデーションライブラリとの連携
    1. Zod(最も人気)
    2. Valibot(軽量代替)
    3. ライブラリ比較表
  10. React でのTS18046
    1. ErrorBoundary の componentDidCatch
    2. useReducer の action が unknown
    3. Context のデフォルト値
    4. サードパーティHookの戻り値
  11. tsconfig.json の関連設定
    1. strict モード
    2. useUnknownInCatchVariables
    3. 段階的移行戦略
  12. まとめ
    1. unknown 型の使いこなしルール
    2. TS18046 vs TS2571 の違い
    3. 関連記事

TS18046エラーとは?

TS18046は、TypeScriptコンパイラがunknown型の値に対して操作を行おうとしたときに発生するエラーです。正式なエラーメッセージは次のとおりです。

error TS18046: ‘X’ is of type ‘unknown’.

日本語に訳すと「‘X’ は unknown 型です」となります。つまり、ある変数が unknown 型であるにもかかわらず、型を絞り込まないまま直接使おうとしたときに発生するエラーです。

エラーメッセージの読み方

TS18046のエラーメッセージは、一定のフォーマットに従っています。具体例で構造を確認しましょう。

エラーメッセージの例
src/index.ts:5:15 - error TS18046: 'e' is of type 'unknown'.

5   console.log(e.message);
                  ~

このエラーメッセージは以下のパーツで構成されています。

パーツ 意味
ファイルパス src/index.ts エラーが発生したファイル
行:列 5:15 エラー箇所(5行目の15文字目)
エラーコード TS18046 TypeScriptのエラー番号
‘X’ e unknown型である変数名
type ‘unknown’ unknown 現在の型(常にunknown)

覚え方:「’変数名‘ is of type ‘unknown’」と表示されたら、「この変数はunknown型なので、型を絞り込んでから使ってください」というTypeScriptからのメッセージです。

unknown型とは何か

unknown 型は TypeScript 3.0 で導入された特殊な型で、「何の型かわからない値」を表現します。any 型と似ていますが、決定的な違いがあります。

特性 any unknown
何でも代入できる はい はい
他の型に代入できる はい(危険) いいえ(安全)
プロパティアクセス 可能(型チェックなし) 不可(型を絞り込む必要あり)
メソッド呼び出し 可能(型チェックなし) 不可(型を絞り込む必要あり)
算術演算 可能 不可
型安全性 なし(型チェック無効化) あり(型チェック強制)

要約すると、unknownは「何でも入れられるけど、中身を確認するまで使えない箱」です。一方、anyは「何でも入れられて、確認なしで使える箱」ですが、型安全性が完全に失われます。

any型 vs unknown型の違い(コード比較)

コードで具体的な違いを見てみましょう。

any型 – 何でもできる(危険)
let value: any = "hello";

// any型は何でもできてしまう(型チェックが無効化される)
value.foo.bar;           // コンパイルOK(実行時エラー!)
value.trim();              // コンパイルOK
value.toFixed(2);          // コンパイルOK(実行時エラー!)
value();                   // コンパイルOK(実行時エラー!)

const num: number = value;  // コンパイルOK(型が間違っている!)
unknown型 – 型を絞り込むまで使えない(安全)
let value: unknown = "hello";

// unknown型は型を絞り込まないと何もできない
value.foo.bar;           // TS18046 エラー
value.trim();              // TS18046 エラー
value.toFixed(2);          // TS18046 エラー
value();                   // TS18046 エラー

const num: number = value;  // TS2322 エラー

// typeof で型を絞り込めば使える
if (typeof value === "string") {
  value.trim();            // OK - string型に絞り込まれた
}

ポイント:unknown型は「型安全なany」です。anyはTypeScriptの型チェックを完全に無効化しますが、unknownは型チェックを強制します。新しいコードではanyの代わりにunknownを使うのがベストプラクティスです。

なぜunknownを使うべきなのか(型安全性のメリット)

any型を使うと、TypeScriptの型チェックが完全に無効化されます。これは便利に見えますが、実行時エラーの原因になります。

any型による実行時エラーの例
function processData(data: any) {
  // TypeScriptはエラーを出さないが、実行時にクラッシュする可能性
  const name = data.user.name.toUpperCase();  // data が null なら?
  const age = data.user.age.toFixed(0);       // age が string なら?
  return { name, age };
}

// コンパイルは通るが、実行時に TypeError: Cannot read properties of null
processData(null);
unknown型なら安全(コンパイル時にエラーを検出)
function processData(data: unknown) {
  // unknown型なので、型を絞り込まないとアクセスできない
  // data.user.name.toUpperCase();  // TS18046 エラー!

  // 型ガードで安全にアクセス
  if (
    typeof data === "object" &&
    data !== null &&
    "user" in data &&
    typeof data.user === "object" &&
    data.user !== null &&
    "name" in data.user &&
    typeof data.user.name === "string"
  ) {
    const name = data.user.name.toUpperCase();  // OK - 安全
    return name;
  }
  return null;
}

unknown型のメリット:コードを書く段階では面倒に感じるかもしれませんが、unknownを使うことで実行時エラーをコンパイル時に検出できます。特にAPIレスポンスやユーザー入力など、外部から入ってくるデータには必ずunknownを使いましょう。

TypeScript 4.4以降でTS18046が増えた理由

TypeScript 4.4で useUnknownInCatchVariables オプションが導入され、strict モードに含まれるようになりました。これにより、catch句の変数がデフォルトでunknown型になります。

TypeScript 4.3 以前(catch の e は any)
try {
  riskyOperation();
} catch (e) {
  // TypeScript 4.3以前: e は any型(型チェックなし)
  console.log(e.message);  // OK(でも安全ではない)
}
TypeScript 4.4 以降 + strict(catch の e は unknown)
try {
  riskyOperation();
} catch (e) {
  // TypeScript 4.4以降 + strict: e は unknown型
  console.log(e.message);  // TS18046 エラー!
}

この変更の理由は、JavaScriptではthrow文で任意の値を投げられるからです。

throwで投げられるのはErrorだけではない
// JavaScript では何でも throw できる
throw new Error("エラーメッセージ");     // Error オブジェクト
throw "文字列エラー";                   // 文字列
throw 42;                                // 数値
throw { code: 404, msg: "Not Found" };  // オブジェクト
throw null;                               // null
throw undefined;                          // undefined

// catch で e.message にアクセスしても安全とは限らない
try {
  throw "文字列エラー";
} catch (e) {
  // e.message → TypeError: e.message is not a function
  // なぜなら e は string であり、message プロパティを持たない
}

注意:TypeScript 4.4以降で strict: true を使っているプロジェクトでは、useUnknownInCatchVariables が自動的に有効になります。既存プロジェクトをアップグレードした際に、大量のTS18046エラーが出ることがあります。

TS18046が発生する主なケース一覧

TS18046エラーが発生する代表的なケースをまとめます。

ケース 説明 解決方法
try-catch の catch 変数 strict モードで e が unknown に instanceof Error で判定
JSON.parse の戻り値 パース結果が any/unknown 型ガード / Zod で検証
fetch APIレスポンス response.json() が unknown 型ガード / バリデーション
unknown 型引数 ジェネリクスで unknown が渡される 型制約 / 型ガード
明示的 unknown 宣言 let x: unknown = … で宣言 typeof / instanceof
Record<string, unknown> 値が unknown のオブジェクト in演算子 + typeof
外部ライブラリの戻り値 ライブラリが unknown を返す ドキュメント確認 + 型ガード

以降のセクションでは、これらのケースを一つずつ詳しく見ていきます。

基本的なケースと解決方法

TS18046エラーの基本的なパターンと、その解決方法を見ていきましょう。unknown型の値を使うには、型の絞り込み(Type Narrowing)を行う必要があります。

unknown型の変数を直接使おうとした場合

最もシンプルなケースです。unknown型の変数に対して、プロパティアクセスやメソッド呼び出しを行うとTS18046が発生します。

error TS18046: ‘value’ is of type ‘unknown’.

NGコード
function handleValue(value: unknown) {
  // unknown型に対するあらゆる操作がTS18046エラーになる
  console.log(value.toString());   // TS18046 エラー
  console.log(value.length);       // TS18046 エラー
  console.log(value + 1);           // TS18046 エラー
  value();                        // TS18046 エラー
}

原因:unknown型は型安全性を保証するために、型を絞り込まない限り一切の操作を許可しません。ただし、typeofinstanceof・等値比較(===)・論理演算子・型アサーション(as)は使用可能です。

typeof による型の絞り込み

typeof 演算子を使うことで、unknown型の値を特定のプリミティブ型に絞り込めます。最も基本的で安全な方法です。

error TS18046: ‘value’ is of type ‘unknown’.

NGコード
function printLength(value: unknown) {
  console.log(value.length);  // TS18046 エラー
}
OKコード(typeof で絞り込み)
function printLength(value: unknown) {
  if (typeof value === "string") {
    console.log(value.length);  // OK - string型に絞り込まれた
    console.log(value.trim());   // OK - string のメソッド
  }
}

typeof で判定できる型の一覧です。

typeof の結果 TypeScript上の型 使用例
"string" string value.trim()
"number" number value.toFixed(2)
"boolean" boolean value ? "yes" : "no"
"bigint" bigint value + 1n
"symbol" symbol value.description
"function" Function value()
"object" object | null nullチェック別途必要
"undefined" undefined 値が存在しない
typeofによる複数型の絞り込み
function formatValue(value: unknown): string {
  if (typeof value === "string") return value.trim();
  if (typeof value === "number") return value.toFixed(2);
  if (typeof value === "boolean") return value ? "true" : "false";
  if (typeof value === "bigint") return value.toString();
  return String(value);
}

注意:typeof null === "object" です。"object" と判定された場合、必ず value !== null のチェックも行いましょう。

instanceof による型の絞り込み

instanceof 演算子を使うと、unknown型の値が特定のクラスのインスタンスかどうかを判定できます。

OKコード(instanceof で絞り込み)
function processUnknown(value: unknown) {
  if (value instanceof Error)   console.log(value.message);        // Error型
  if (value instanceof Date)    console.log(value.toISOString());  // Date型
  if (value instanceof RegExp)  console.log(value.source);         // RegExp型
  if (value instanceof Map)     console.log(value.size);           // Map型
  if (value instanceof Set)     console.log(value.size);           // Set型
  if (value instanceof Promise) value.then(v => console.log(v)); // Promise型
}

型アサーション(as)での解決

型アサーション(as)を使えば、unknown型を特定の型として扱えます。ただし型の安全性は開発者の責任です。

型アサーション vs 型ガード
// 型アサーション(安全性は自己責任)
function getLength(data: unknown): number {
  return (data as string).length;  // dataがstringでなければ実行時エラー
}

// 型ガードと組み合わせて安全に(推奨)
function getLengthSafe(data: unknown): number | null {
  if (typeof data === "string") return data.length;
  if (Array.isArray(data)) return data.length;
  return null;
}

注意:型アサーション(as)は型チェックを上書きします。可能な限り型ガードを使いましょう。

型ガード関数(ユーザー定義型ガード)での解決

ユーザー定義型ガードは、value is Type という戻り値型を持つ関数で、複雑な型チェックを再利用可能にカプセル化できます。

ユーザー定義型ガードの例
interface User {
  name: string;
  age: number;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    typeof (value as User).name === "string" &&
    "age" in value &&
    typeof (value as User).age === "number"
  );
}

function greetUser(user: unknown) {
  if (isUser(user)) {
    console.log(`Hello, ${user.name}!`);  // OK
    console.log(`Age: ${user.age}`);       // OK
  }
}

assertsキーワードによる型ガード

asserts キーワードを使った型ガードは、条件を満たさない場合にエラーをスローし、以降のコードで型が絞り込まれることを保証します。

asserts型ガードの例
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function processInput(input: unknown) {
  assertIsString(input);
  // この行以降、input は string 型
  console.log(input.trim());   // OK
  console.log(input.length);  // OK
}

is vs asserts の違い:value is Type はif文の中で型を絞り込みます。asserts value is Type は関数が正常に返れば以降のすべてのコードで型が絞り込まれます。

解決方法の比較まとめ

方法 安全性 適用場面 注意点
typeof プリミティブ型 objectはnull含む
instanceof クラスインスタンス プレーンオブジェクト不可
型ガード関数 複雑な型の判定 正確な実装が必要
asserts型ガード 前提条件の検証 例外をスロー
型アサーション(as) 型が確実なとき 実行時エラーのリスク
バリデーションライブラリ 最高 外部データの検証 追加依存が必要

try-catch の catch でのTS18046

TS18046エラーに遭遇する最も多いケースが、try-catch文のcatch句です。TypeScript 4.4以降、strictモードでは catch の変数が自動的に unknown 型になります。

catch(e) の e が unknown になる理由

JavaScriptでは throwあらゆる値を投げることができます。Errorオブジェクトだけでなく、文字列、数値、null、undefined、さらにはオブジェクトも投げられます。そのため、catch句の変数の型を Error と仮定するのは安全ではありません。

error TS18046: ‘e’ is of type ‘unknown’.

NGコード(strictモードで発生)
try {
  JSON.parse("invalid json");
} catch (e) {
  // strict モードでは e は unknown 型
  console.log(e.message);  // TS18046 エラー!
  console.log(e.stack);    // TS18046 エラー!
  console.log(e.name);     // TS18046 エラー!
}

解決方法1: instanceof Error でチェック(推奨)

最も安全で推奨される方法は、instanceof Error で型を絞り込むことです。

OKコード(instanceof Error)
try {
  JSON.parse("invalid json");
} catch (e) {
  if (e instanceof Error) {
    console.log(e.message);  // OK - "Unexpected token i..."
    console.log(e.stack);    // OK - スタックトレース
    console.log(e.name);     // OK - "SyntaxError"
  } else {
    // Error以外が投げられた場合のフォールバック
    console.log("Unknown error:", e);
  }
}

解決方法2: カスタムエラークラスの判定

アプリケーション固有のカスタムエラークラスを使っている場合、instanceof でより具体的に型を絞り込めます。

カスタムエラークラスの判定
// カスタムエラークラスの定義
class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public endpoint: string
  ) {
    super(message);
    this.name = "ApiError";
  }
}

class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

// catch で型を絞り込む
try {
  await fetchUserData();
} catch (e) {
  if (e instanceof ApiError) {
    console.log(e.statusCode);  // OK - ApiError固有プロパティ
    console.log(e.endpoint);    // OK
  } else if (e instanceof ValidationError) {
    console.log(e.field);       // OK - ValidationError固有プロパティ
  } else if (e instanceof Error) {
    console.log(e.message);     // OK - 一般的なError
  } else {
    console.log("Unknown error:", e);
  }
}

解決方法3: エラーメッセージの安全な取得パターン

Error以外の値が投げられる可能性も考慮した、堅牢なエラーメッセージ取得パターンです。

安全なエラーメッセージ取得
// パターン1: シンプルな取得
function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

// パターン2: スタックトレース付き
function getErrorDetail(error: unknown): { message: string; stack?: string } {
  if (error instanceof Error) {
    return { message: error.message, stack: error.stack };
  }
  return { message: String(error) };
}

// 使用例
try {
  riskyOperation();
} catch (e) {
  const msg = getErrorMessage(e);
  console.error(`操作に失敗しました: ${msg}`);
}

解決方法4: isError型ガードの自作

プロジェクト全体で再利用できる isError 型ガード関数と、unknown を Error に変換する toError ユーティリティを作成します。

isError / toError ユーティリティ
// 型ガード: unknown が Error かどうか
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

// unknown を Error に変換(Error でなければラップ)
function toError(value: unknown): Error {
  if (value instanceof Error) return value;
  if (typeof value === "string") return new Error(value);
  if (
    typeof value === "object" &&
    value !== null &&
    "message" in value &&
    typeof (value as { message: unknown }).message === "string"
  ) {
    return new Error((value as { message: string }).message);
  }
  return new Error(String(value));
}

// 使用例
try {
  riskyOperation();
} catch (e) {
  const error = toError(e);
  console.error(error.message);  // OK - 常に Error 型
}

解決方法5: catch(e: any) で回避(非推奨)

catch句で e に明示的に any 型を付けることでTS18046を回避できます。ただし、型安全性が失われるため非推奨です。

非推奨:catch(e: any)
// 非推奨:any で回避
try {
  riskyOperation();
} catch (e: any) {
  console.log(e.message);  // OK(でも型安全ではない)
}

// 推奨:instanceof でチェック
try {
  riskyOperation();
} catch (e) {
  if (e instanceof Error) {
    console.log(e.message);  // OK(型安全)
  }
}

注意:catch(e: any) は型チェックを無効化するため、TypeScriptを使う意味が薄れます。instanceof Error で型を絞り込むか、toError ユーティリティを使いましょう。

実務でよく使うcatchパターン集

実務で頻繁に使うcatchパターンをまとめます。

実務パターン集
// パターン1: APIリクエスト
async function fetchData() {
  try {
    const res = await fetch("/api/data");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (e) {
    const message = e instanceof Error ? e.message : "Unknown error";
    console.error(`API Error: ${message}`);
    throw e; // 再スロー
  }
}

// パターン2: ファイル読み込み(Node.js)
try {
  const data = await fs.readFile("config.json", "utf-8");
} catch (e) {
  if (e instanceof Error && "code" in e) {
    const nodeErr = e as NodeJS.ErrnoException;
    if (nodeErr.code === "ENOENT") {
      console.log("File not found");
    }
  }
}

// パターン3: ログ送信(エラーを握りつぶさない)
try {
  processOrder(order);
} catch (e) {
  const error = e instanceof Error ? e : new Error(String(e));
  logToSentry(error);  // Error型を要求するログサービス
  throw error;
}

ポイント:catch句でのTS18046解決は、instanceof Error が最もシンプルで安全です。プロジェクト全体で統一するために、getErrorMessagetoError のようなユーティリティ関数を作成しておくと効率的です。

JSON.parse でのTS18046

JSON.parse() の戻り値は any 型ですが、strict設定やプロジェクトのルールによって unknown として扱うべきケースがあります。外部から受け取ったJSON文字列をパースした結果を安全に使う方法を解説します。

JSON.parseの戻り値を安全に扱う

JSON.parse() の戻り値は TypeScript の型定義上は any ですが、実際にはどんな値が入っているかわかりません。unknown として扱い、型を検証してから使うのがベストプラクティスです。

error TS18046: ‘parsed’ is of type ‘unknown’.

NGコード(unknownとして受け取った場合)
const jsonStr = '{"name": "Alice", "age": 30}';
const parsed: unknown = JSON.parse(jsonStr);

console.log(parsed.name);  // TS18046 エラー
console.log(parsed.age);   // TS18046 エラー
OKコード(型ガードで検証)
interface UserData {
  name: string;
  age: number;
}

function isUserData(value: unknown): value is UserData {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    typeof (value as UserData).name === "string" &&
    "age" in value &&
    typeof (value as UserData).age === "number"
  );
}

const parsed: unknown = JSON.parse(jsonStr);

if (isUserData(parsed)) {
  console.log(parsed.name);  // OK - "Alice"
  console.log(parsed.age);   // OK - 30
}

Zod によるバリデーション(推奨)

手動で型ガードを書くのは面倒で、複雑な型では間違いやすくなります。Zod を使えば、スキーマ定義と型推論を同時に行えます。

Zod によるJSON.parseの検証
import { z } from "zod";

// スキーマ定義(型定義と検証ロジックを同時に定義)
const UserSchema = z.object({
  name: z.string(),
  age: z.number().int().positive(),
});

// 型はスキーマから自動推論される
type User = z.infer<typeof UserSchema>;
// => { name: string; age: number }

// 安全にパース
const jsonStr = '{"name": "Alice", "age": 30}';
const result = UserSchema.safeParse(JSON.parse(jsonStr));

if (result.success) {
  const user = result.data;  // 型: User
  console.log(user.name);     // OK - "Alice"
  console.log(user.age);      // OK - 30
} else {
  console.error(result.error.issues);
}

safeParse ユーティリティの自作

バリデーションライブラリを使わない場合の、安全なJSONパースユーティリティです。

safeParse ユーティリティ
type ParseResult<T> =
  | { success: true; data: T }
  | { success: false; error: Error };

function safeParse<T>(
  json: string,
  guard: (value: unknown) => value is T
): ParseResult<T> {
  try {
    const parsed: unknown = JSON.parse(json);
    if (guard(parsed)) {
      return { success: true, data: parsed };
    }
    return { success: false, error: new Error("Validation failed") };
  } catch (e) {
    return { success: false, error: e instanceof Error ? e : new Error(String(e)) };
  }
}

// 使用例
const result = safeParse(jsonStr, isUserData);
if (result.success) {
  console.log(result.data.name);  // OK - 型安全
}

ポイント:JSON.parseの戻り値は実質的にunknownです。型アサーション(as)で済ませがちですが、外部データに対しては必ずバリデーションを行いましょう。Zodを使うのが最も簡潔で安全です。

API レスポンス・外部データでのTS18046

外部から取得するデータは、型が保証されていません。fetch API、axios、WebSocket、localStorage など、外部データソースからの値を安全に扱う方法を解説します。

fetch APIレスポンスの安全な型付け

response.json() の戻り値は Promise<any> ですが、unknownとして扱うべきです。

fetch APIの安全な型付け
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  const json: unknown = await res.json();
  return UserSchema.parse(json);  // バリデーション + 型付け
}

axiosレスポンスの安全な型付け

axiosでは response.data がデフォルトで any です。ジェネリクスで型を指定できますが、実際の検証は行われません。

axiosの安全な型付け
import axios from "axios";
import { z } from "zod";

// NGパターン: ジェネリクスだけでは実際の検証が行われない
const res = await axios.get<User>("/api/user");
// res.data は User型だが、実際のデータが正しい保証はない

// OKパターン: Zodで検証
async function fetchUserSafe(id: number): Promise<User> {
  const res = await axios.get(`/api/users/${id}`);
  return UserSchema.parse(res.data);  // 実際にデータを検証
}

localStorage / sessionStorage の値

localStorage.getItem()string | null を返します。JSON.parse と組み合わせる場合、unknown として安全に扱う必要があります。

localStorageの安全な読み取り
function getFromStorage<T>(
  key: string,
  guard: (value: unknown) => value is T
): T | null {
  const raw = localStorage.getItem(key);
  if (raw === null) return null;

  try {
    const parsed: unknown = JSON.parse(raw);
    return guard(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

// 使用例
const settings = getFromStorage("settings", isSettings);

WebSocket メッセージの安全な型付け

WebSocketメッセージの型付け
const MessageSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("chat"), text: z.string(), userId: z.string() }),
  z.object({ type: z.literal("join"), userId: z.string() }),
  z.object({ type: z.literal("leave"), userId: z.string() }),
]);

ws.addEventListener("message", (event) => {
  const parsed = MessageSchema.safeParse(JSON.parse(event.data));
  if (!parsed.success) return;

  const msg = parsed.data;
  switch (msg.type) {
    case "chat":  console.log(msg.text);    break;
    case "join":  console.log(msg.userId);  break;
    case "leave": console.log(msg.userId);  break;
  }
});

外部データの原則:fetch・axios・WebSocket・localStorage・URLSearchParams・環境変数など、外部から入ってくるデータはすべてunknownとして扱うべきです。型アサーション(as)で済ませるのはアンチパターンです。バリデーションを通してから使いましょう。

ジェネリクスと unknown のTS18046

ジェネリクス関数の型引数がunknownに推論されたり、Record<string, unknown> のような型を使うとTS18046が発生することがあります。

ジェネリクス関数の型引数がunknown

型引数を明示的に指定しない場合、TypeScriptは引数からジェネリクスの型を推論します。推論できない場合は unknown になります。

error TS18046: ‘item’ is of type ‘unknown’.

ジェネリクスとunknown
// T に制約がないため、unknown として扱われるケースがある
function processItem<T>(item: T) {
  // T に制約がないため、プロパティアクセスは不可
  console.log(item.name);  // エラー: T にnameプロパティがない
}

// 解決: extends で型制約を追加
function processItem<T extends { name: string }>(item: T) {
  console.log(item.name);  // OK - 制約によりnameが保証
}

Record<string, unknown> の扱い

Record<string, unknown> は「キーがstring、値がunknownのオブジェクト」を表します。値にアクセスすると unknown 型が返るため、型ガードが必要です。

error TS18046: ‘value’ is of type ‘unknown’.

Record<string, unknown>の操作
const config: Record<string, unknown> = {
  host: "localhost",
  port: 3000,
  debug: true,
};

// NGコード
const host = config.host.toUpperCase();  // TS18046 エラー

// OKコード: typeofで絞り込み
const hostValue = config.host;
if (typeof hostValue === "string") {
  console.log(hostValue.toUpperCase());  // OK
}

// ヘルパー関数で安全に取得
function getString(obj: Record<string, unknown>, key: string): string | undefined {
  const val = obj[key];
  return typeof val === "string" ? val : undefined;
}

function getNumber(obj: Record<string, unknown>, key: string): number | undefined {
  const val = obj[key];
  return typeof val === "number" ? val : undefined;
}

const host = getString(config, "host");  // string | undefined
const port = getNumber(config, "port");  // number | undefined

unknown[] 配列の操作

unknown[] の各要素を操作するには、要素ごとに型を絞り込む必要があります。

unknown[]の安全な操作
const items: unknown[] = ["hello", 42, true, { name: "Alice" }];

// 文字列だけを抽出
const strings = items.filter(
  (item): item is string => typeof item === "string"
);  // 型: string[]

// 数値だけを抽出して合計
const numbers = items.filter(
  (item): item is number => typeof item === "number"
);  // 型: number[]
const sum = numbers.reduce((a, b) => a + b, 0);  // OK

// map で各要素を安全に変換
const labels = items.map((item) => {
  if (typeof item === "string") return `str: ${item}`;
  if (typeof item === "number") return `num: ${item}`;
  return `other: ${String(item)}`;
});

ポイント:Array.filter でユーザー定義型ガードを使えば、unknown[] から特定の型の要素だけを型安全に抽出できます。コールバックの戻り値型に item is Type を指定するのがコツです。

型の絞り込みテクニック(完全版)

TS18046を解決するために使える型の絞り込み(Type Narrowing)テクニックをすべてまとめます。

typeof ガード

typeof 演算子はプリミティブ型の判定に使います。前セクションで紹介した通り、"string""number""boolean""bigint""symbol""function""object""undefined" の8種類を判定できます。

typeof ガードの完全パターン
function stringify(value: unknown): string {
  switch (typeof value) {
    case "string":    return value;                // string
    case "number":    return value.toString();       // number
    case "boolean":   return value ? "true" : "false"; // boolean
    case "bigint":    return value.toString();       // bigint
    case "symbol":    return value.toString();       // symbol
    case "function":  return "[Function]";        // Function
    case "object":    return JSON.stringify(value); // object | null
    case "undefined": return "undefined";         // undefined
  }
}

in 演算子ガード

in 演算子を使うと、オブジェクトに特定のプロパティが存在するかを判定できます。Discriminated Union パターンと組み合わせると強力です。

in 演算子ガード
function handleResponse(value: unknown) {
  // まず object かつ non-null であることを確認
  if (typeof value !== "object" || value === null) return;

  // in 演算子でプロパティの存在をチェック
  if ("data" in value) {
    // value は { data: unknown } と推論される
    console.log(value.data);  // OK(ただし data は unknown)
  }

  if ("error" in value && "message" in value) {
    console.log(value.error, value.message);  // OK
  }
}

等値チェック(=== / !==)

厳密等値演算子を使って、特定の値と比較することで型を絞り込めます。

等値チェックによる絞り込み
function handleValue(value: unknown) {
  // null / undefined の除外
  if (value === null) return "null";
  if (value === undefined) return "undefined";

  // 特定のリテラル値と比較
  if (value === true) return "boolean true";
  if (value === 0) return "zero";
  if (value === "") return "empty string";

  return String(value);
}

Array.isArray() による配列判定

typeof では配列を判定できません(typeof [] === "object")。Array.isArray() を使うことで、unknown型から安全に配列を抽出できます。

Array.isArray()による絞り込み
function processData(data: unknown) {
  if (Array.isArray(data)) {
    // data は any[] に絞り込まれる
    console.log(data.length);   // OK
    data.forEach(item => {      // OK
      console.log(item);          // item は any
    });
  }
}

// 型付き配列かどうかチェックする型ガード
function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === "string");
}

Discriminated Union パターン

共通の判別プロパティ(discriminant)を持つユニオン型を使うことで、型を安全に絞り込めます。

Discriminated Unionパターン
type ApiResult =
  | { status: "success"; data: User }
  | { status: "error"; message: string; code: number };

// status プロパティで型を判別
function handleResult(result: ApiResult) {
  switch (result.status) {
    case "success":
      console.log(result.data.name);  // OK - User型
      break;
    case "error":
      console.log(result.message);   // OK - string型
      console.log(result.code);      // OK - number型
      break;
  }
}

// unknown から Discriminated Union に変換する型ガード
function isApiResult(value: unknown): value is ApiResult {
  if (typeof value !== "object" || value === null) return false;
  if (!("status" in value)) return false;
  const s = (value as { status: unknown }).status;
  return s === "success" || s === "error";
}

ポイント:型絞り込みの詳細は【TypeScript】型の絞り込み 完全ガイドで体系的に解説しています。TS18046の解決には、これらのテクニックを状況に応じて組み合わせることが重要です。

unknown を安全に扱うユーティリティ関数集

プロジェクト全体で再利用できるユーティリティ関数を紹介します。これらを utils/typeGuards.ts のようなファイルにまとめておくと、unknown型の値を効率的に扱えます。

プリミティブ型ガード

isString / isNumber / isBoolean
export function isString(value: unknown): value is string {
  return typeof value === "string";
}

export function isNumber(value: unknown): value is number {
  return typeof value === "number" && !Number.isNaN(value);
}

export function isBoolean(value: unknown): value is boolean {
  return typeof value === "boolean";
}

export function isNonNullObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

isError / toError

isError / toError / getErrorMessage
export function isError(value: unknown): value is Error {
  return value instanceof Error;
}

export function toError(value: unknown): Error {
  if (value instanceof Error) return value;
  if (typeof value === "string") return new Error(value);
  return new Error(String(value));
}

export function getErrorMessage(error: unknown): string {
  if (error instanceof Error) return error.message;
  return String(error);
}

hasProperty(プロパティ存在チェック)

hasProperty ユーティリティ
// 特定のプロパティが存在し、指定した型であることをチェック
export function hasProperty<K extends string>(
  obj: unknown,
  key: K
): obj is Record<K, unknown> {
  return typeof obj === "object" && obj !== null && key in obj;
}

// 使用例
function process(data: unknown) {
  if (hasProperty(data, "name")) {
    // data は Record<"name", unknown> 型
    const name = data.name;  // unknown
    if (typeof name === "string") {
      console.log(name.toUpperCase());  // OK
    }
  }
}

narrowType(汎用型絞り込み)

narrowType ユーティリティ
// Result型(成功 or 失敗)
type NarrowResult<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

// 型ガード関数を渡して unknown を安全に変換
export function narrowType<T>(
  value: unknown,
  guard: (v: unknown) => v is T,
  errorMsg = "Type guard failed"
): NarrowResult<T> {
  if (guard(value)) {
    return { ok: true, value };
  }
  return { ok: false, error: errorMsg };
}

// 使用例
const result = narrowType(data, isUser, "Invalid user data");
if (result.ok) {
  console.log(result.value.name);  // OK - User型
}

まとめ:これらのユーティリティ関数を utils/typeGuards.ts にまとめておくと、プロジェクト全体でunknown型の値を統一的に扱えます。チーム開発では特に効果的です。

バリデーションライブラリとの連携

unknown型の値を手動で検証するのは面倒でエラーが起きやすいです。バリデーションライブラリを使えば、スキーマ定義型推論を同時に行えます。主要なライブラリを比較します。

Zod(最も人気)

Zodは TypeScript ファーストのバリデーションライブラリで、最も広く使われています。スキーマから型が自動推論されるため、型定義の重複がありません。

Zod の基本パターン
import { z } from "zod";

// スキーマ定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["admin", "user", "guest"]),
  createdAt: z.string().datetime(),
});

// 型を自動推論
type User = z.infer<typeof UserSchema>;

// parse: 失敗時はエラーをスロー
const user = UserSchema.parse(unknownData);  // User型 or ZodError

// safeParse: 成功/失敗を Result で返す
const result = UserSchema.safeParse(unknownData);
if (result.success) {
  console.log(result.data.name);  // OK - User型
} else {
  console.error(result.error.flatten());
}

Valibot(軽量代替)

Valibotは Zod と似たAPIですが、バンドルサイズが非常に小さいのが特徴です。Tree-shakingに最適化されています。

Valibot の基本パターン
import * as v from "valibot";

const UserSchema = v.object({
  id: v.number(),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
});

type User = v.InferOutput<typeof UserSchema>;

const result = v.safeParse(UserSchema, unknownData);
if (result.success) {
  console.log(result.output.name);  // OK
}

ライブラリ比較表

ライブラリ バンドルサイズ 型推論 特徴
Zod ~13KB (gzip) 自動推論 最も人気、豊富なAPI
Valibot ~1KB (gzip) 自動推論 超軽量、Tree-shaking対応
io-ts ~5KB (gzip) 自動推論 fp-ts連携、関数型スタイル
Yup ~15KB (gzip) InferType フォームバリデーション向け
自作型ガード 0KB 手動 依存なし、小規模向き

おすすめ:新しいプロジェクトでは Zod がおすすめです。バンドルサイズが気になる場合は Valibot を検討してください。小規模なプロジェクトや外部依存を避けたい場合は自作の型ガードで十分です。

React でのTS18046

React + TypeScriptのプロジェクトでもTS18046に遭遇することがあります。代表的なケースと解決方法を紹介します。

ErrorBoundary の componentDidCatch

クラスコンポーネントの componentDidCatch のエラー引数は Error 型ですが、コンポーネント外でcatchした場合はunknownになることがあります。

ErrorBoundary でのエラーハンドリング
class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { error: Error | null }
> {
  state = { error: null as Error | null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    // error は Error 型(React が保証)
    console.error(error.message);           // OK
    console.error(errorInfo.componentStack);  // OK
  }

  render() {
    if (this.state.error) {
      return <div>Error: {this.state.error.message}</div>;
    }
    return this.props.children;
  }
}

useReducer の action が unknown

useReducer で型引数を適切に指定しないと、action が unknown になることがあります。

useReducer の型安全な使い方
// Discriminated Union で action を定義
type Action =
  | { type: "INCREMENT" }
  | { type: "DECREMENT" }
  | { type: "SET"; payload: number };

interface State {
  count: number;
}

// reducer に明示的に型を指定
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "INCREMENT": return { count: state.count + 1 };
    case "DECREMENT": return { count: state.count - 1 };
    case "SET":       return { count: action.payload }; // OK
  }
}

// useReducer は reducer 関数から型を自動推論
const [state, dispatch] = useReducer(reducer, { count: 0 });
dispatch({ type: "SET", payload: 10 });  // OK - 型安全

Context のデフォルト値

createContext のデフォルト値を nullundefined にすると、Context の値がunknown的に扱われることがあります。

型安全な Context パターン
interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// null を初期値にし、カスタムHookで非null保証
const AuthContext = createContext<AuthContextType | null>(null);

export function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === null) {
    throw new Error("useAuth must be used within AuthProvider");
  }
  return context;  // AuthContextType(non-null)
}

サードパーティHookの戻り値

サードパーティライブラリのHookが unknown を返す場合の対処法です。

サードパーティHookのラッパー
import { z } from "zod";

// unknown を返す外部Hook のラッパー
function useTypedQuery<T>(
  key: string,
  schema: z.ZodType<T>,
  fetcher: () => Promise<unknown>
) {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    fetcher()
      .then((raw) => {
        const result = schema.safeParse(raw);
        if (result.success) setData(result.data);
        else setError(new Error("Validation failed"));
      })
      .catch((e) => setError(e instanceof Error ? e : new Error(String(e))));
  }, [key]);

  return { data, error };
}

// 使用例
const { data, error } = useTypedQuery("users", UserSchema, fetchUsers);
// data は User | null(型安全)

tsconfig.json の関連設定

TS18046の発生はTypeScriptコンパイラの設定に大きく影響されます。関連する設定項目を解説します。

strict モード

strict: true を設定すると、複数のstrict系オプションが一括で有効になります。TS18046に関連するのは useUnknownInCatchVariables です。

tsconfig.json
{
  "compilerOptions": {
    // strict: true は以下をすべて有効にする
    "strict": true,

    // strict に含まれる個別オプション
    // "strictNullChecks": true,
    // "strictFunctionTypes": true,
    // "strictBindCallApply": true,
    // "strictPropertyInitialization": true,
    // "noImplicitAny": true,
    // "noImplicitThis": true,
    // "alwaysStrict": true,
    // "useUnknownInCatchVariables": true  ← TS18046の原因
  }
}

useUnknownInCatchVariables

この設定が true のとき、catch句の変数が unknown 型になります。strict: true に含まれるため、strictモードを使っているプロジェクトでは自動的に有効です。

設定 catch(e) の型 TS18046
useUnknownInCatchVariables: false any 発生しない
useUnknownInCatchVariables: true unknown 発生する
strict: true(暗黙的にtrue) unknown 発生する

段階的移行戦略

既存プロジェクトで strict: true に移行する際、大量のTS18046エラーが出ることがあります。段階的に移行する方法を紹介します。

段階的移行: tsconfig.json
// ステップ1: strict を有効にし、一時的に useUnknownInCatchVariables を無効化
{
  "compilerOptions": {
    "strict": true,
    "useUnknownInCatchVariables": false  // 一時的に無効化
  }
}

// ステップ2: catch句を順次修正
// - instanceof Error でチェック
// - getErrorMessage ユーティリティを導入

// ステップ3: すべての catch句を修正したら useUnknownInCatchVariables を削除
{
  "compilerOptions": {
    "strict": true
    // useUnknownInCatchVariables は strict に含まれるので不要
  }
}

ポイント:useUnknownInCatchVariables を無効にするのはあくまで一時的な対応です。最終的には有効にして、catch句を正しく型安全に書くことを目指しましょう。tsconfig.jsonの詳細はtsconfig.json 完全ガイドを参照してください。

まとめ

TS18046エラー('X' is of type 'unknown')は、unknown型の値を型を絞り込まずに使おうとしたときに発生します。この記事の内容をまとめます。

TS18046 解決フローチャート

  1. エラーメッセージを確認 – どの変数が unknown 型か特定する
  2. unknown の出所を確認 – catch句?JSON.parse?APIレスポンス?ジェネリクス?
  3. 型の絞り込み方法を選ぶ
    • プリミティブ値 → typeof
    • クラスインスタンス → instanceof
    • オブジェクトのプロパティ → in 演算子
    • 複雑な型 → ユーザー定義型ガード(value is Type
    • 外部データ → バリデーションライブラリ(Zod推奨)
  4. 型ガードの後でプロパティにアクセス

unknown 型の使いこなしルール

ルール 説明
any より unknown を使う 型安全性を維持するため
外部データは unknown で受ける API、JSON.parse、localStorage等
型ガードで絞り込んでからアクセス typeof、instanceof、in、ユーザー定義型ガード
catch句では instanceof Error e が Error 以外の可能性があるため
型アサーション(as)は最後の手段 型の安全性が開発者の責任になる
複雑なデータはZodで検証 スキーマ定義と型推論を同時に

TS18046 vs TS2571 の違い

TS18046と混同しやすいエラーに TS2571Object is of type 'unknown')があります。

エラーコード メッセージ 発生条件
TS18046 'X' is of type 'unknown' unknown型の変数にプロパティアクセス
TS2571 Object is of type 'unknown' unknown型のオブジェクトを操作

どちらも解決方法は同じです。型ガード(typeof / instanceof / in / ユーザー定義型ガード)で型を絞り込んでからアクセスしてください。

関連記事

TypeScriptの型システムをさらに深く学びたい方は、以下の記事もあわせてお読みください。

TypeScript 関連記事