Interactive

型システム

型定義があれば、ClaudeCodeはコードの意図を理解して型エラーも自分で直せるようになります。弊社では、いくつかの型安全に関する制約をシステムプロンプトに含めています。

弊社の推奨ルール

  1. interfaceではなくtypeを使う - 一貫性のある型定義で統一
  2. anyではなくunknownを使う - 型安全性を保証
  3. as演算子ではなくZodでバリデーション - 実行時の型保証
  4. 空文字列ではなくnullを使う - 値の欠如を明確に表現
  5. optionalではなくnullableを使う - プロパティの存在を保証

ClaudeCodeでの利用

型定義からのコード生成

ClaudeCodeは型定義を読み取って、適切なメソッドやバリデーションを生成できます。

「UserResponseの型定義からCRUD操作を実装して」
→ 型定義を読み取って適切なメソッドを自動生成
「このエラーを修正:型 'string | undefined' を型 'string' に割り当てることはできません」
→ 早期リターンやnullチェックで型を絞り込む修正を提案
「Zodスキーマを作成。nullableを優先、optionalは避ける」
→ 弊社ルールに従った型定義を自動生成
「既存の型定義を維持したまま、このメソッドをリファクタリング」
→ 型安全性を保証しながらコードを改善

interfaceではなくtypeを使う

一貫性のある型定義を実現するため、interfaceではなくtypeを使用します。interfaceは宣言のマージなど不要な機能を持ち、予期しない動作の原因となります。

// 推奨しない:interfaceの予期しない宣言マージ
interface User {
  name: string;
}
interface User {  // 同名interfaceは自動的にマージされる
  age: number;
}

// 推奨:typeは明示的で安全
type User = {
  id: number;
  name: string;
  email: string | null;
};

anyではなくunknownを使う

型安全性を保つため、anyではなくunknownを使用します。anyは型チェックを完全に無効化し、バグの温床となります。

// 推奨しない:anyは危険
async function fetchDataBad(): Promise<any> {
  const response = await fetch('/api/user');
  const data: any = await response.json();
  return data.nonExistent.method(); // 実行時エラーになる
}

// 推奨:unknownで安全に型を絞り込む
async function fetchDataGood(): Promise<User | null> {
  const response = await fetch('/api/user');
  const data: unknown = await response.json();
  
  const result = UserSchema.safeParse(data);
  return result.success ? result.data : null;
}

as演算子ではなくZodでバリデーション

型の強制変換は避け、Zodでのバリデーションを使用します。as演算子は型の不整合を隠蔽し、実行時エラーの原因となります。

// 推奨しない:as演算子で強制変換
const user = {} as User; // 実際には空オブジェクト
console.log(user.name); // undefined(実行時エラーの可能性)

// 推奨:Zodでバリデーション
import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().nullable(),
});

const result = UserSchema.safeParse(data);
if (result.success) {
  console.log(result.data.name); // 型安全で確実
}

唯一の例外はBranded Type作成時のみです。

空文字列ではなくnullを使う

値の欠如を表現する際は、空文字列ではなくnullを使用します。空文字列では「意図的に空」と「未入力」の区別ができません。

// 推奨しない:空文字列で欠如を表現
type UserBad = {
  name: string;
  bio: string; // ""で未入力を表現?
};

// 推奨:nullで明示的に表現
type UserGood = {
  name: string;
  bio: string | null; // 未入力は明確にnull
};

optionalではなくnullableを使う

プロパティの存在を必須として、値の欠如はnullで表現します。optionalは「プロパティ自体がない」状態を作り、処理が複雑になります。

// 推奨しない:optional
type UserBad = {
  name: string;
  email?: string; // プロパティ自体がない可能性
};

// 推奨:nullable
type UserGood = {
  name: string;
  email: string | null; // プロパティは必ず存在、値がnullの可能性
};

ジェネリクスを使った汎用的な型定義

APIレスポンスの共通パターンです。エラーハンドリングとデータ取得を統一的に扱えます。

// 汎用的なレスポンス型
type ApiResponse<T> = {
  data: T | null;
  error: string | null;
};

// ページネーション対応
type PagedData<T> = {
  items: T[];
  total: number;
};

// 具体的な使用例
type UserResponse = ApiResponse<User>;
type UserListResponse = ApiResponse<PagedData<User>>;

このパターンにより、API全体で一貫したエラーハンドリングが可能になります。

z.inferでスキーマから型を自動生成する

Zodスキーマから型を自動生成することで、スキーマと型定義の二重管理を避けます。スキーマが変更されれば型も自動的に更新されるため、保守性が向上します。

import { z } from "zod";

// Zodスキーマを定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().nullable(),
  createdAt: z.date(),
  profile: z.object({
    bio: z.string().nullable(),
    avatar: z.string().nullable(),
  }).nullable(),
});

スキーマ定義後、z.inferを使って自動的に型を生成します。手動で型を書く必要がなく、スキーマの変更が自動的に型に反映されます。

// z.inferで型を自動生成(手動で型を書く必要なし)
type User = z.infer<typeof UserSchema>;
// 上記は以下と同等だが、スキーマと同期される
// type User = {
//   id: number;
//   name: string;
//   email: string | null;
//   createdAt: Date;
//   profile: { bio: string | null; avatar: string | null; } | null;
// };

ネストした型の抽出や配列スキーマからの型生成も簡単に行えます。

// ネストした型も自動で抽出可能
type UserProfile = z.infer<typeof UserSchema>['profile'];

// 配列スキーマからの型生成
const UsersSchema = z.array(UserSchema);
type Users = z.infer<typeof UsersSchema>; // User[]

このパターンにより、スキーマ変更時の型定義の更新漏れを防げ、コードの一貫性が保たれます。

よくある問題

型の絞り込みが不十分

nullableな値を扱う際は、早期リターンで絞り込みます。型チェックを通すだけでなく、コードの可読性も向上します。

// エラーになる例
function processUser(user: { id: number; name: string } | null) {
  return user.name; // エラー: userがnullの可能性
}

// 早期リターンで解決
function processUser(user: { id: number; name: string } | null) {
  if (!user) return null;
  return user.name;
}

unknownから特定の型への変換

APIレスポンスはZodでバリデーションします。型安全性と実行時チェックを同時に実現できます。

import { z } from "zod";

// スキーマ定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().nullable(),
});

type User = z.infer<typeof UserSchema>;

// APIからデータ取得
async function fetchUser(id: number): Promise<User | null> {
  const response = await fetch(`/api/users/${id}`);
  const data: unknown = await response.json();
  
  const result = UserSchema.safeParse(data);
  if (!result.success) return null;
  
  return result.data;
}

配列メソッドでの型推論エラー

配列操作もZodで型を保証します。filterやmapチェーンでも型安全性を維持できます。

import { z } from "zod";

// ベーススキーマと管理者スキーマ
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  role: z.enum(["admin", "user"]).nullable().default(null),
});

const AdminSchema = UserSchema.extend({
  role: z.literal("admin"),
});

type Admin = z.infer<typeof AdminSchema>;

定義したスキーマを使って管理者のみを抽出します。map、filter、mapのチェーンで型安全性を維持しながら配列を処理できます。

// 管理者のみを抽出
const admins = users
  .map(u => AdminSchema.safeParse(u))
  .filter(r => r.success)
  .map(r => r.data!);

ライブラリの型定義がない場合の対処

@types/ライブラリ名をインストールします。存在しない場合はdeclare moduleで一時的に対応します。

型の絞り込みが効かない場合の対処

Zodのsafeparseを使用してバリデーションと型の絞り込みを同時に行います。

ジェネリクスの制約が書けない場合の対処

extendsで制約を明示的に記述します。複雑な場合は条件型(Conditional Types)を検討します。

型ガードが使えない場合の対処

Zodのsafeparseで代替します。カスタム型ガードが必要な場合は、Zodスキーマでラップします。

optionalとnullableの使い分けがわからない場合の対処

弊社ではnullableで統一します。値の欠如はnullで表現し、プロパティ自体は必須とします。