TypeScript における `any` と `unknown` の違いと使い所(Zodに関しても)
はじめに
TypeScript で API 通信や JSON データを扱う際、型安全性の確保は常に課題となります。any
型を使用すると型チェックを無効化することによる予期せぬランタイムエラーになってしまいます。特にAPI のレスポンス形式が変更された際にコンパイルエラーにならないまま本番環境でエラーが発生する問題が出てくることもあります。
そこでunknown
型です。TypeScript 3.0 で導入されたこの型は「型安全な any」として、型の不明な値を扱いながらも型チェックを強制できる画期的な機能です。コンパイル時にエラーを検出できるようになり、コードの信頼性が大きく向上します。
現在では、Zod などの型検証ライブラリが主流となりつつあり、スキーマ定義一つで型の定義からバリデーション、変換までを一括して行えるようになっています。多くの場合、Zod を使えばunknown
型を明示的に扱わなくても型安全なコードを書けます。
しかし、TypeScript の型システムの基本を理解することは依然として重要です。なぜなら:
- Zod などのライブラリを使わない環境でも型安全なコードを書く必要がある場合がある
- ライブラリのバンドルサイズを小さくしたい場合は標準機能のみで実装したほうが良い
- TypeScript の型システムの根本的な理解は、どのようなライブラリを使う場合でも有益である
- レガシーコードを扱う際に
any
型とunknown
型の違いを理解していると改善できる箇所が見つけやすい
この記事では、今でも理解しておくべきany
型とunknown
型の違いと適切な使い所についてまとめました。
基本的な違い
特性 | any |
unknown |
---|---|---|
型安全性 | 低い(型チェックを回避) | 高い(型チェックを強制) |
操作の自由度 | 高い(どんな操作も許可) | 低い(型の絞り込みが必要) |
代入の制約 | どの型の変数にも代入可能 | 厳格(any 型とunknown 型のみに代入可能) |
型推論 | 型情報の喪失 | 型情報の保持を強制 |
any
型
any
型は、TypeScript の型チェックを事実上無効にする特殊な型です。
特徴
let valueAny: any = 10;
// どんな操作も許可される
valueAny.foo(); // コンパイルエラーにならない
valueAny.bar.baz(); // コンパイルエラーにならない
valueAny = "文字列"; // 型の変更も自由
valueAny = { x: 10 }; // オブジェクトへの変更も自由
// どの型の変数にも代入可能
const str: string = valueAny; // コンパイルエラーにならない
const num: number = valueAny; // コンパイルエラーにならない
メリット
- 既存の JavaScript コードとの統合が容易
- 型が複雑または不明な場合の一時的な対処が可能
- マイグレーション初期段階での利用が便利
デメリット
- 型安全性が失われる
- コンパイル時のエラーチェックが無効になる
- ランタイムエラーを引き起こす可能性が高い
- IDE の自動補完サポートが制限される
unknown
型
unknown
型は「型安全なany
」とも呼ばれ、TypeScript 3.0 で導入されました。
特徴
let valueUnknown: unknown = 10;
// 直接操作は許可されない
// valueUnknown.foo(); // コンパイルエラー
// valueUnknown.bar.baz(); // コンパイルエラー
// const str: string = valueUnknown; // コンパイルエラー
// 型チェックを行った後なら操作可能
if (typeof valueUnknown === "string") {
const str: string = valueUnknown; // OK
console.log(valueUnknown.toUpperCase()); // OK
}
// 型アサーションを使う方法
const strValue = valueUnknown as string;
メリット
- 型安全性を維持しながら未知の値を扱える
- 明示的な型チェックを強制し、バグを防止
- コンパイラによる型の流れの追跡が可能
デメリット
- 型チェックやキャストが必要で、少し冗長になる場合がある
- 型ガード関数の作成が必要な場合がある
使い所の比較
any
の適切な使い所
-
外部ライブラリとの連携初期段階
- 型定義が不完全または存在しない外部ライブラリの使用時
-
段階的な型付け導入時
- 大規模な JavaScript コードを TypeScript に移行する初期段階
-
プロトタイピング
- 素早く試作品を作る際の一時的な対処
-
どうしても型が定義できない複雑なケース
- 最後の手段として限定的に使用
unknown
の適切な使い所
-
API 応答や JSON パース結果
- 外部から取得したデータの型が不明な場合
const responseData: unknown = await fetch("/api/data").then((r) => r.json());
// 型チェック後に使用
if (
typeof responseData === "object" &&
responseData &&
"users" in responseData
) {
const users = responseData.users;
// ...
}
- 型安全なエラーハンドリング
try {
// 何らかの処理
} catch (error: unknown) {
// 型を絞り込んでから処理
if (error instanceof Error) {
console.error(error.message);
} else {
console.error(String(error));
}
}
- 汎用的なユーティリティ関数
function safeStringify(value: unknown): string {
return typeof value === "string" ? value : JSON.stringify(value);
}
- ユーザー入力の処理
function processUserInput(input: unknown) {
if (typeof input === "string") {
// 文字列として処理
} else if (Array.isArray(input)) {
// 配列として処理
} else {
// その他のケース
}
}
実践的なコード例
実務でのany
型とunknown
型の使用例を見ていきましょう。特に Web 開発では、API との通信やユーザー入力の処理など、不明な型を扱う場面が多くあります。
パース関数の実装
問題:JSON パース結果の型安全性
API 通信やローカルストレージからのデータ取得など、外部から JSON 形式のデータを取得する際、JSON.parse()
の戻り値は本来any
型です。これがどのような構造になっているかは TypeScript のコンパイル時には検証できません。
any
型を使った実装(非推奨)
解決方法 1:まず、any
型を使った一般的な実装を見てみましょう。
// anyを使った実装 - 型安全性が低い
function parseJSONUnsafe(jsonString: string): any {
return JSON.parse(jsonString);
}
// 使用例
const data = parseJSONUnsafe('{"name": "田中", "age": 30}');
このように実装すると、どんな操作も許可されてしまいます。
// 問題点: コンパイルエラーにならないが実行時にエラーになる可能性
console.log(data.name); // OK
console.log(data.nonExistentProperty.foo); // 実行時エラー!typescriptはこのエラーを検出できない
unknown
型を使った実装
解決方法 2:unknown
型を使うことで、型チェックを強制できます。
// unknownを使った実装 - 型安全
function parseJSONSafe(jsonString: string): unknown {
return JSON.parse(jsonString);
}
// 使用例
const safeData = parseJSONSafe('{"name": "田中", "age": 30}');
// この時点では型が不明なのでプロパティにアクセスできない
// console.log(safeData.name); // コンパイルエラー
型ガード関数の実装
unknown
型の値を安全に使用するには、型ガード関数を作成します。
// 期待する型の定義
interface User {
name: string;
age: number;
}
// 型ガード関数の実装
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"name" in obj &&
"age" in obj &&
typeof (obj as any).name === "string" &&
typeof (obj as any).age === "number"
);
}
型ガードを使って安全にデータにアクセスできます。
// 型チェック後に安全に使用
if (isUser(safeData)) {
// この時点でsafeDataはUser型として扱われる
console.log(safeData.name); // OK
console.log(safeData.age); // OK
// 型の恩恵を受けられる(自動補完や型チェックが効く)
const upperName = safeData.name.toUpperCase(); // OK
const yearOfBirth = new Date().getFullYear() - safeData.age; // OK
}
解決方法 3:Zod を使った実装
より強力で簡潔な方法として、Zod のようなバリデーションライブラリを使用する方法があります。Zod は型定義とバリデーションを一度に行えるため、コードの量が減り、メンテナンスしやすくなります。
import { z } from "zod";
// スキーマ定義(型定義とバリデーションルールを同時に定義)
const userSchema = z.object({
name: z.string(),
age: z.number().int().positive(),
});
// 型を自動で推論
type User = z.infer<typeof userSchema>;
// パース関数
function parseJSONWithZod(jsonString: string): User {
const parsedData = JSON.parse(jsonString);
// パースと同時にバリデーション実行、不正なデータは例外を投げる
return userSchema.parse(parsedData);
}
// 例外を投げない安全なバージョン
function safeParseJSONWithZod(jsonString: string): {
success: boolean;
data?: User;
error?: z.ZodError;
} {
try {
const parsedData = JSON.parse(jsonString);
const result = userSchema.safeParse(parsedData);
return result;
} catch (error) {
return {
success: false,
error: error instanceof z.ZodError ? error : undefined,
};
}
}
// 使用例
try {
const user = parseJSONWithZod('{"name": "田中", "age": 30}');
console.log(`${user.name}さんは${user.age}歳です`); // OK、型安全
} catch (error) {
console.error("不正なデータ形式です");
}
// safeParseの使用例
const result = safeParseJSONWithZod('{"name": "田中", "age": "三十"}');
if (result.success) {
const user = result.data;
console.log(`${user.name}さんは${user.age}歳です`);
} else {
console.error("バリデーションエラー:", result.error?.errors);
}
Zod の利点は型の定義とバリデーションが一体化していることと、より複雑な型変換も簡単に行えることです。例えば、文字列から数値への変換なども自動的に行えます。
// 型変換機能を使ったスキーマ
const userSchemaWithCoercion = z.object({
name: z.string(),
age: z.coerce.number().int().positive(), // 文字列から数値に自動変換
birthDate: z.coerce.date(), // 文字列から日付に自動変換
});
// 文字列でも自動的に変換してくれる
const user = userSchemaWithCoercion.parse({
name: "田中",
age: "30", // 文字列でも数値に変換
birthDate: "1990-01-01", // 文字列でも日付に変換
});
console.log(typeof user.age); // number
console.log(user.birthDate instanceof Date); // true
API リクエスト処理の例
問題:API 応答データの型安全性
フロントエンドアプリケーションでは、API から受け取るデータの型安全性が重要です。特に TypeScript では、API レスポンスが期待する形式かどうかをコンパイル時に検証することはできません。
共通の型定義
まず、共通の型定義を行います。
// ユーザーの型定義
interface User {
id: string;
name: string;
email: string;
role: "admin" | "user" | "guest";
createdAt: string;
}
// APIレスポンスの共通型定義
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
any
型を使った実装(非推奨)
解決方法 1:any
型を使った一般的な API 通信の実装です。
// anyを使った危険な実装
async function fetchUserUnsafe(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
const json = await response.json();
// 何のチェックもなしにjsonを返す
return json;
}
この実装の問題点は、使用時に型の安全性が全く保証されないことです。
// 使用例 - 危険な実装
async function displayUserUnsafe(userId: string) {
const userData = await fetchUserUnsafe(userId);
// 型チェックがないので、存在しないプロパティにアクセスしても警告されない
console.log(`ユーザー: ${userData.name}, ロール: ${userData.role}`);
// APIの仕様変更で userData.role が undefined になっていた場合
// 実行時エラーが発生する可能性がある
if (userData.role === "admin") {
showAdminPanel();
}
}
unknown
型と型ガードを使った実装
解決方法 2:unknown
型と型ガード関数を使って、型安全な実装を行います。
// unknown型を使った安全な実装
async function fetchUser<T>(url: string): Promise<T> {
const response = await fetch(url);
// レスポンスステータスのチェック
if (!response.ok) {
throw new Error(`APIエラー: ${response.status} ${response.statusText}`);
}
// いったんunknown型として受け取る
const json: unknown = await response.json();
// 型ガード関数を使用して型を確認
if (isApiResponse<T>(json)) {
return json.data;
}
throw new Error("APIレスポンスの形式が不正です");
}
// ApiResponse<T>の型ガード
function isApiResponse<T>(obj: unknown): obj is ApiResponse<T> {
return (
typeof obj === "object" &&
obj !== null &&
"success" in obj &&
"data" in obj &&
typeof (obj as any).success === "boolean"
);
}
// ユーザー型のためのより詳細な型ガード関数
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"name" in obj &&
"email" in obj &&
"role" in obj &&
"createdAt" in obj &&
typeof (obj as any).id === "string" &&
typeof (obj as any).name === "string" &&
typeof (obj as any).email === "string" &&
["admin", "user", "guest"].includes((obj as any).role) &&
typeof (obj as any).createdAt === "string" &&
!isNaN(new Date((obj as any).createdAt).getTime())
);
}
この実装を使うことで、型安全に API データを扱えます。
// 使用例 - 安全な実装
async function displayUser(userId: string) {
try {
const userData = await fetchUser<User>(`/api/users/${userId}`);
// この時点でuserDataはUser型として扱われる
console.log(`ユーザー: ${userData.name}, ロール: ${userData.role}`);
// 型安全性が確保されている
if (userData.role === "admin") {
showAdminPanel();
}
// 日付の処理も型安全
const createdDate = new Date(userData.createdAt);
console.log(`登録日: ${createdDate.toLocaleDateString()}`);
} catch (error: unknown) {
// エラーハンドリングも型安全
if (error instanceof Error) {
console.error(`エラー: ${error.message}`);
} else {
console.error("不明なエラーが発生しました");
}
}
}
// 管理者パネルを表示する関数
function showAdminPanel() {
// 実装省略
}
解決方法 3:Zod を使った型安全な API 実装
Zod を使うと、API レスポンスのバリデーションと型付けを同時に行えます。さらに、JSON から日付型への変換なども自動的に行えます。
import { z } from "zod";
// Zodスキーマの定義
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.string().transform(str => new Date(str)), // 文字列から日付に変換
});
// レスポンススキーマ
const apiResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema,
message: z.string().optional(),
});
// 型の推論
type User = z.infer<typeof userSchema>;
type UserResponse = z.infer<typeof apiResponseSchema(userSchema)>;
// Zodを使ったAPI通信関数
async function fetchUserWithZod(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`APIエラー: ${response.status} ${response.statusText}`);
}
const json = await response.json();
// レスポンス形式のバリデーション
const validatedResponse = apiResponseSchema(userSchema).parse(json);
// この時点でdataはUser型として扱われ、createdAtはDate型になっている
return validatedResponse.data;
}
// 使用例
async function displayUserWithZod(userId: string) {
try {
const user = await fetchUserWithZod(userId);
// 型安全かつ日付は既にDate型
console.log(`ユーザー: ${user.name}, ロール: ${user.role}`);
console.log(`登録日: ${user.createdAt.toLocaleDateString()}`); // Date型のメソッドが使える
if (user.role === "admin") {
showAdminPanel();
}
} catch (error) {
if (error instanceof z.ZodError) {
// バリデーションエラーの詳細な処理が可能
console.error("データ形式エラー:", error.errors);
} else if (error instanceof Error) {
console.error(`APIエラー: ${error.message}`);
} else {
console.error("不明なエラーが発生しました");
}
}
}
Zod を使うことで得られる追加的なメリット:
- より洗練されたバリデーション - 長さ、フォーマット(メールアドレス、URL)、範囲などの制約を簡単に定義できる
- データ変換の自動化 - 文字列から日付や数値への変換など
- エラーメッセージの詳細化 - どのフィールドがどのような理由で不正かがわかる
- コードの簡潔さ - 型ガード関数を個別に実装する必要がない
フォーム入力データの処理例
問題:ユーザー入力データの型安全性
ユーザーからの入力データは形式が不明瞭で、型安全に処理する必要があります。
unknown
型と型ガードを使った実装
解決方法 1:まず、フォームデータの型を定義します。
// フォームデータの型定義
interface ContactForm {
name: string;
email: string;
message: string;
priority: "high" | "medium" | "low";
}
次に、DOM からデータを取得する関数を実装します。この関数は戻り値をunknown
型とすることで、型安全性を確保します。
// DOMからフォームの値を取得する関数
function getFormData(): unknown {
const nameInput = document.getElementById("name") as HTMLInputElement;
const emailInput = document.getElementById("email") as HTMLInputElement;
const messageInput = document.getElementById(
"message"
) as HTMLTextAreaElement;
const prioritySelect = document.getElementById(
"priority"
) as HTMLSelectElement;
// 素のオブジェクトとして値を収集
return {
name: nameInput.value,
email: emailInput.value,
message: messageInput.value,
priority: prioritySelect.value,
};
}
型ガード関数を実装して、取得したデータが正しい形式かどうかをチェックします。
// ContactForm型かどうかを確認する型ガード関数
function isContactForm(obj: unknown): obj is ContactForm {
if (typeof obj !== "object" || obj === null) return false;
const form = obj as Partial<ContactForm>;
// 必須フィールドの存在チェック
if (typeof form.name !== "string" || form.name.trim() === "") return false;
if (typeof form.email !== "string" || !form.email.includes("@")) return false;
if (typeof form.message !== "string") return false;
// 優先度の値チェック
if (
typeof form.priority !== "string" ||
!["high", "medium", "low"].includes(form.priority)
) {
return false;
}
return true;
}
最後に、フォーム送信処理を実装します。型ガードを使って型安全性を確保します。
// フォーム送信処理
function handleSubmit() {
const formData = getFormData();
if (!isContactForm(formData)) {
// バリデーションエラー処理
showError("フォームに不正な値があります。");
return;
}
// この時点でformDataはContactForm型として扱われる
console.log(
`${formData.name}さんからの${formData.priority}優先度のメッセージ`
);
// 安全にAPIに送信
submitToApi(formData)
.then(() => showSuccess("メッセージを送信しました。"))
.catch((error: unknown) => {
if (error instanceof Error) {
showError(`送信エラー: ${error.message}`);
} else {
showError("不明なエラーが発生しました。");
}
});
}
// APIにデータを送信する関数
async function submitToApi(data: ContactForm): Promise<void> {
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`API エラー: ${response.status}`);
}
}
// ユーティリティ関数
function showError(message: string): void {
const errorElement = document.getElementById("error-message");
if (errorElement) {
errorElement.textContent = message;
errorElement.style.display = "block";
}
}
function showSuccess(message: string): void {
const successElement = document.getElementById("success-message");
if (successElement) {
successElement.textContent = message;
successElement.style.display = "block";
}
}
解決方法 2:Zod を使ったフォームバリデーション
Zod を使うと、より簡潔にフォームバリデーションを実装できます。
import { z } from "zod";
// フォームスキーマの定義
const contactFormSchema = z.object({
name: z.string().min(1, "名前は必須です"),
email: z.string().email("有効なメールアドレスを入力してください"),
message: z.string().min(10, "メッセージは10文字以上入力してください"),
priority: z.enum(["high", "medium", "low"], {
errorMap: () => ({ message: "優先度を選択してください" }),
}),
});
// 型の推論
type ContactForm = z.infer<typeof contactFormSchema>;
// DOMからフォームの値を取得する関数
function getFormData() {
const nameInput = document.getElementById("name") as HTMLInputElement;
const emailInput = document.getElementById("email") as HTMLInputElement;
const messageInput = document.getElementById(
"message"
) as HTMLTextAreaElement;
const prioritySelect = document.getElementById(
"priority"
) as HTMLSelectElement;
// 素のオブジェクトとして値を収集
return {
name: nameInput.value,
email: emailInput.value,
message: messageInput.value,
priority: prioritySelect.value,
};
}
// フォーム送信処理
function handleSubmitWithZod() {
const formData = getFormData();
// バリデーションの実行
const result = contactFormSchema.safeParse(formData);
if (!result.success) {
// バリデーションエラーの詳細な処理
const errors = result.error.errors;
const errorMessages = errors
.map((err) => `${err.path}: ${err.message}`)
.join("\n");
showError(`フォームに不正な値があります:\n${errorMessages}`);
return;
}
// 型安全なデータ
const validatedData = result.data;
console.log(
`${validatedData.name}さんからの${validatedData.priority}優先度のメッセージ`
);
// 安全にAPIに送信
submitToApi(validatedData)
.then(() => showSuccess("メッセージを送信しました。"))
.catch((error: unknown) => {
if (error instanceof Error) {
showError(`送信エラー: ${error.message}`);
} else {
showError("不明なエラーが発生しました。");
}
});
}
unknown 型と Zod の使い分け
unknown
型とZod
のどちらを使うべきかは、プロジェクトの要件によって異なります。
unknown 型のメリット
- 標準ライブラリのみで実装可能 - 外部依存がない
- 細かいカスタマイズが可能 - バリデーションロジックを完全に制御できる
- バンドルサイズの増加なし - 追加のライブラリが不要
Zod のメリット
- コードの簡潔さ - 型定義とバリデーションが統合され、コード量が減る
- 豊富なバリデーションルール - 組み込みの検証ルールが多数ある
- エラーメッセージの充実 - 詳細なエラー情報が得られる
- 型変換の自動化 - 文字列から数値や日付などへの変換が簡単
- 複雑なデータ構造の対応 - ネストされたオブジェクトや配列も簡単に扱える
おすすめの使い分け
-
小規模プロジェクトや依存関係を最小限にしたい場合:
unknown
型と型ガード - 大規模プロジェクトや複雑なバリデーションが必要な場合: Zod のような型検証ライブラリ
プロジェクトが成長するにつれて、unknown
型と手動の型ガードから、より強力な Zod のようなライブラリに移行するのも一つの選択肢です。
これらの例からわかるように、unknown
型を適切に使用すること、または Zod のようなライブラリを活用することで、次のようなメリットがあります:
- 型安全性の確保 - コンパイル時にエラーを検出
- 明示的な型チェック - コードの意図が明確になる
- 予期せぬ実行時エラーの防止 - 事前に型の確認を強制
- デバッグの容易さ - 型の問題を早期に発見
- コードの品質向上 - バリデーションの一貫性が高まる
ベストプラクティス
-
any
の使用を最小限に-
any
型の使用はコードベース内で明確に理由がある場合のみにする - ESLint の
@typescript-eslint/no-explicit-any
ルールを活用して制限する
-
-
unknown
を優先的に使用- 型が不明な値を扱う場合は、
any
ではなくunknown
を使用する - 明示的な型チェックを行ってから値を操作する
- 型が不明な値を扱う場合は、
-
型ガード関数を作成
- 複雑なオブジェクト構造には専用の型ガード関数を実装する
- 再利用可能な型チェックを実装して、冗長なコードを減らす
-
as
キャストは慎重に- 型アサーションは必要な場合のみ使用し、できるだけ型ガードで代替する
-
as unknown as T
のような複数段階のキャストは避ける
-
型定義ファイルの活用
- 外部ライブラリを使用する場合は、可能な限り型定義ファイル(
@types/...
)を導入する - 型定義がない場合は、自前の宣言ファイル(
.d.ts
)を作成する
- 外部ライブラリを使用する場合は、可能な限り型定義ファイル(
結論
any
型とunknown
型はどちらも不明な型を扱うためのものですが、アプローチが大きく異なります:
-
any
は型チェックを完全に回避し、どんな操作も許可する「型なし」に近い状態です。便利ですが危険性が高いため、使用は最小限にすべきです。 -
unknown
は型安全性を維持しながら未知の値を扱うための手段で、明示的な型チェックを強制します。実務ではunknown
を優先的に使うべきです。
コードの保守性と安全性を高めるためには、any
型の使用を最小限に抑え、代わりにunknown
型と適切な型ガードを活用することが重要です。これにより、コンパイル時により多くのエラーを検出し、ランタイムエラーを減らすことができます。
ですが...
やっぱり結論としてはZodで良い?
バンドルサイズを極限まで小さくしたい場合や、非常にシンプルなケースではunknown型と手動の型ガードが適していることもありますが、多くの実践的なシナリオではZodのみのアプローチで十分対応。
参考サイト
Discussion