32blogby Studio Mitsu

Claude Code × TypeScript|型安全なコード生成のコツ

Claude CodeにTypeScriptの厳格な型安全コードを生成させるコツを全公開。strictモード設定・Zod連携・型エラーを自動修正させるループまで実践的に解説する。

by omitsu21 min read
Claude CodeTypeScripttype safetyZodstrict mode
目次

Claude Codeに「APIレスポンスをパースして」と頼んだら、こういうコードが返ってきた。僕は最初、これで型安全になっていると勘違いしていた。

typescript
const data = response.data as any
const user = data.user as any
const name = (data.user as any).name as string

as any が3回。型情報が完全に捨てられている。これでは型チェックの意味がない。

このパターンは実際のプロジェクトでバグの温床になる。APIのフィールド名が userName から displayName に変わったのに、as any がミスマッチを握り潰して、コンパイラも実行時も何も言わない。ユーザーから「名前が表示されない」と報告が来て初めて気付く——そんなケースが後を絶たない。

TypeScriptのコードを書かせるとき、Claude Codeはデフォルトで「とりあえず動くコード」を優先する。型安全より実行できることを選ぶ。それを変えるには、型安全を明示的に要求する設定と指示が必要だ。

この記事を読めば次のことがわかる。

  • as any が生成される根本原因と解決策
  • tsconfig.json の strict 設定を CLAUDE.md で強制する方法
  • Zod スキーマ設計を Claude Code に任せる手順
  • 型エラーを自動修正させるフィードバックループ
  • 型安全なコード生成のための CLAUDE.md テンプレート
as any 乱用型情報なし設定strict 強制tsconfig + CLAUDE.md検証Zod バリデーションランタイム型安全達成型安全なコードany ゼロ

Claude Codeがanyを乱用するのはなぜか?

Claude Codeが any を使いたがる理由は主に3つある。

1. 型情報がない状態でコードを完成させようとする

APIレスポンスの型定義がない、ライブラリの型定義が不完全、など「型がわからない」状況でClaude Codeは any で埋めようとする。型エラーを出さずにコードを完成させることを優先するからだ。

2. 指示に型安全の要件が含まれていない

「パースして」「変換して」という指示に型の要件は含まれていない。Claude Codeは指示された機能を実現することに集中して、型の厳格さは後回しになる。

3. プロジェクトの型設定が伝わっていない

tsconfig.json の設定がClaude Codeに伝わっていないと、strictモードの存在を知らずに緩い型で書いてしまう。

これらを解決するアプローチは「プロジェクト設定を先に伝える」「型を最初から定義させる」「型チェックを必須にする」の3点だ。この設定を入れると、Claude Codeの出力に含まれる as any が大幅に減る。


tsconfig.jsonのstrict設定をCLAUDE.mdで強制するには?

CLAUDE.mdに以下の内容を書くと、Claude Codeがコードを生成するたびに型安全の要件を参照するようになる。

まずはtsconfig.jsonの設定。TypeScript 5.9以降、tsc --init のデフォルトに noUncheckedIndexedAccessexactOptionalPropertyTypes が含まれるようになった。

json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

そしてCLAUDE.mdに書くコード生成のルール。

markdown
## TypeScript設定

### コード生成のルール
1. `as any` は禁止。どんな場合も使わない
2. `as Type` のキャストは型ガードか Zod バリデーション後のみ許可
3. 関数の引数・戻り値には必ず型注釈を付ける
4. `any` の代わりに `unknown` を使い、型ガードで絞り込む
5. 型推論に頼れる場面では明示的な型注釈は不要(過剰な注釈もNG)

### 検証コマンド
コード生成後は必ず `npx tsc --noEmit` を実行する。
型エラーが出たら修正してから提出すること。

この設定がCLAUDE.mdにあると、プロンプトごとに「anyを使わないで」と言わなくて済む。tsconfig.json の内容をCLAUDE.mdに転記しておくのが特に効く。


ZodスキーマをClaude Codeに設計させるにはどうすればいいか?

外部APIのレスポンスを扱う場合、Zodスキーマを先に定義してからパースコードを生成させる流れが安全だ。

Step 1: APIのレスポンス例を渡してZodスキーマを生成させる

以下のAPIレスポンス例からZodスキーマを生成してください。

APIレスポンス例:
{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com",
  "role": "admin" | "user" | "guest",
  "createdAt": "2026-01-01T00:00:00Z",
  "profile": {
    "bio": "Developer",
    "avatarUrl": "https://example.com/avatar.jpg" | null
  }
}

要件:
- すべてのフィールドに型を付ける
- role は union 型で定義する
- createdAt は z.coerce.date() でDate型に変換する
- nullable なフィールドは .nullable() で明示する
- スキーマから型を推論して export する

生成されたスキーマ。

typescript
import { z } from 'zod'

const ProfileSchema = z.object({
  bio: z.string(),
  avatarUrl: z.string().url().nullable(),
})

const UserRoleSchema = z.enum(['admin', 'user', 'guest'])

export const UserSchema = z.object({
  id: z.number().int().positive(),
  name: z.string().min(1),
  email: z.string().email(),
  role: UserRoleSchema,
  createdAt: z.coerce.date(),
  profile: ProfileSchema,
})

export type User = z.infer<typeof UserSchema>
export type UserRole = z.infer<typeof UserRoleSchema>

Step 2: スキーマを使ったパース関数を生成させる

上記の UserSchema を使って、fetch のレスポンスをパースする関数を書いてください。

要件:
- パース失敗時は ZodError をスローする
- unknown 型で受け取って Zod でバリデーションする
- `as any` は使わない
typescript
import { UserSchema, type User } from './schemas/user'

export async function fetchUser(userId: number): Promise<User> {
  const response = await fetch(`/api/users/${userId}`)

  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.status}`)
  }

  const json: unknown = await response.json()
  return UserSchema.parse(json)
}

response.json() の戻り値を unknown で受けて、ZodでバリデーションしてからTypedな値を返す。これが as any を使わずに外部データを扱う正しいパターンだ。


tsc --noEmit パスを必須条件にするにはどう書くか?

型エラーを生成コードに残させないために、CLAUDE.mdに次の一文を加える。

markdown
## コード提出前の必須チェック

コードを生成・修正したら、必ず `npx tsc --noEmit` を実行すること。
型エラーがゼロになるまでコードを修正してから回答すること。
型エラーが残ったままのコードは提出しない。

この「型エラーがゼロになるまで修正」という制約が重要だ。これがないと、Claude Codeは型エラーが出ていても「機能としては動く」という判断でコードを返してくることがある。

--noEmit フラグはJavaScriptファイルを出力せずに型チェックだけを実行するオプションだ。CIでの型チェックにも使える。

json
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch"
  }
}

型エラーをClaudeに自動修正させるループはどう回すか?

tsc --noEmit の出力をそのままClaudeに渡して修正させるループが効率的だ。

bash
# 型エラーを取得
npx tsc --noEmit 2>&1

出力例。

src/utils/parseResponse.ts:12:5 - error TS2322: Type 'any' is not assignable to type 'User'.
src/utils/parseResponse.ts:18:3 - error TS7006: Parameter 'data' implicitly has an 'any' type.

Found 2 errors.

これをClaudeに渡す。

以下のTypeScriptエラーを修正してください。

エラー:
src/utils/parseResponse.ts:12:5 - error TS2322: Type 'any' is not assignable to type 'User'.
src/utils/parseResponse.ts:18:3 - error TS7006: Parameter 'data' implicitly has an 'any' type.

修正方針:
- `as any` を使わずに修正する
- unknown 型 + 型ガードか Zod を使う
- 修正後に `tsc --noEmit` でエラーがゼロになることを確認してから回答する

このループを回し続けると、最終的に型エラーがゼロのコードが出てくる。エラーが複数あるときは一度に渡してOKで、Claudeは全エラーをまとめて修正しようとする。


外部APIレスポンスの型付けはClaude Codeにどう任せるか?

SwaggerやOpenAPI仕様書がある場合は、それをClaudeに渡してZodスキーマに変換させるのが最速だ。

以下のOpenAPI仕様から、TypeScript + Zodのスキーマを生成してください。

仕様(抜粋):
---
components:
  schemas:
    Product:
      type: object
      required:
        - id
        - name
        - price
      properties:
        id:
          type: integer
        name:
          type: string
          maxLength: 100
        price:
          type: number
          minimum: 0
        category:
          type: string
          enum: [electronics, clothing, food]
        stock:
          type: integer
          nullable: true
---

要件:
- z.infer<typeof Schema> で型を export する
- バリデーション制約(maxLength・minimum)もZodに反映する
- nullable は .nullable() で表現する

OpenAPI仕様が複雑な場合は openapi-zod-client などのツールも選択肢になるが、Claudeに変換させる方が細かい調整がしやすい。


Prismaと連携して型推論を最大化するには?

PrismaはスキーマからTypeScriptの型を自動生成してくれる。Claude Codeにこの型を活用させる指示を書いておく。詳しくはClaude Code × Prisma API自動生成も参照。

markdown
## Prismaの型活用ルール(CLAUDE.mdに追記)

- Prismaが生成した型(`PrismaClient`・`Prisma.UserGetPayload` など)を積極的に使う
- `User` 型は `import type { User } from '@prisma/client'` から参照する
- クエリの戻り値には `Prisma.UserGetPayload<{include: {posts: true}}>` のような型を使う
- 独自の型定義でPrismaの型を上書きしない

Prismaの型を使ったコード例。

typescript
import type { Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'

type UserWithPosts = Prisma.UserGetPayload<{
  include: { posts: { include: { tags: true } } }
}>

export async function getUserWithPosts(userId: string): Promise<UserWithPosts | null> {
  return prisma.user.findUnique({
    where: { id: userId },
    include: {
      posts: {
        include: { tags: true },
      },
    },
  })
}

Prismaが生成する型を使えば、DBのスキーマ変更がTypeScriptの型エラーとして即座に検出できる。Claude Codeに「Prismaの型を使って」と一言添えるだけで、この恩恵を受けられる。


型安全なコード生成のためのCLAUDE.mdテンプレートはどう書くか?

ここまでの内容をまとめた CLAUDE.md テンプレートを公開する。このままプロジェクトに追加して使える。

markdown
## TypeScript型安全ポリシー

### 絶対禁止
- `as any` の使用(理由を問わず禁止)
- `@ts-ignore` の使用(`@ts-expect-error` は理由コメント付きで許可)
- 型注釈なしの関数定義(引数・戻り値の型は必須)

### 外部データの型付け
- 外部API・JSONは `unknown` で受けて Zod でバリデーション
- `response.json() as SomeType` のキャストは禁止
- Zodスキーマから `z.infer<typeof Schema>` で型を export する

### tsconfig.json準拠
strict: true / noImplicitAny: true / strictNullChecks: true / noUncheckedIndexedAccess: true

### 提出前チェック(必須)
コード生成・修正後は `npx tsc --noEmit` を実行し、エラーゼロを確認してから回答する。

### Zodの使い方
- スキーマ定義は `src/schemas/` ディレクトリに集約する
- `.parse()` はバリデーション失敗時にエラーをスロー(意図的な動作)
- `.safeParse()` はエラーハンドリングが必要な場合に使う
- スキーマファイルには型も一緒に export する

ジェネリック型やユーティリティ型を正確に生成させるには?

Claude Codeにジェネリック型を書かせると、制約が甘い <T> を乱用しがちだ。CLAUDE.mdにジェネリクスのルールを明記すると精度が上がる。

CLAUDE.mdに追記するジェネリクスルール:

markdown
# ジェネリクスルール
- ジェネリック型パラメータには必ず制約をつける(`<T>` ではなく `<T extends Base>`)
- ユーティリティ型(Pick, Omit, Partial)を積極的に使い、新しい型定義を減らす
- 条件付き型は2段階まで。3段階以上のネストは関数オーバーロードに分割する

Claude Codeに条件付き型を生成させるプロンプト例:

APIレスポンスの型を作って。以下の条件:
- GET /users → User[]
- GET /users/:id → User
- POST /users → User
- エンドポイントごとに戻り値の型が変わるconditional typeにする

生成される型の例:

typescript
type ApiEndpoint = "GET /users" | "GET /users/:id" | "POST /users";

type ApiResponse<E extends ApiEndpoint> = E extends "GET /users"
  ? User[]
  : E extends "GET /users/:id"
    ? User
    : E extends "POST /users"
      ? User
      : never;

よくある質問

as unknown as Type は安全?

as any よりはマシだが、型チェッカーを迂回している点は同じだ。正しいアプローチはZodか型ガードで検証してからキャストすること。as unknown as Type はランタイムで形状を検証済みだがTypeScriptの推論が追いつかない場合に限り、理由をコメントに添えて使う。

サードパーティの @types パッケージが any を使っている場合は?

ライブラリ呼び出しを薄いアダプター関数でラップして、出力を unknown で受けてZodか型ガードでバリデーションする。any を境界に閉じ込めて、自分のコードベースに漏れさせないのが鉄則だ。例えば someLib.parse()any を返すなら、function safeParse(input: string): MyType { return MySchema.parse(someLib.parse(input)) } と書く。

Zodは内部データにも使うべき?外部APIだけ?

TypeScriptの型チェックが効いている内部データ(関数間の受け渡し等)にはZodは不要。Zodが威力を発揮するのは 信頼境界 — APIレスポンス、ユーザー入力、環境変数、JSON.parse() の出力など、型チェックの外から来るデータだ。

JSON.parse() の戻り値を安全に型付けするには?

JSON.parse() はデフォルトで any を返す。安全なパターンは const raw: unknown = JSON.parse(jsonString) の後に MySchema.parse(raw) でバリデーション。JSON.parse(str) as MyType は絶対にやらない。

strictNullChecks を有効にすると既存コードが壊れる?

ほぼ確実にYes。別ブランチで有効にして tsc --noEmit を実行し、エラーを段階的に修正するのがおすすめだ。有効化すると、本番で潜在的にバグになりうるnull安全性の問題が多数浮かび上がることが多い。

@ts-ignore の代わりに @ts-expect-error を使うべき?

使うべきだ。@ts-expect-error は抑制していたエラーが修正されると逆にエラーになるので、不要な抑制がコードに残らない。必ずコメントを添える: // @ts-expect-error — ライブラリの型定義にinternalMethodが公開されていないCLAUDE.mdのルールにも「@ts-expect-error のみ許可」と明記しておく。

strict モードで tsc のコンパイル速度は落ちる?

ほとんど変わらない。strictフラグはチェックパスを追加するが、10万行規模のプロジェクトでも差は5%以下が典型的だ。noUncheckedIndexedAccess はインデックスアクセスに | undefined を追加するので対処すべき型エラーは増えるが、それはパフォーマンスコストではなく正確性の恩恵だ。

Reactのイベント型はstrictモードでどう扱う?

Reactの組み込みイベント型を使う: React.ChangeEvent<HTMLInputElement>React.FormEvent<HTMLFormElement> など。Claude Codeが (e: any) => を生成してきたら、CLAUDE.mdに「Reactイベントハンドラは React.*Event の型付きイベントを使う。any は禁止」と追記する。


関連記事


まとめ

Claude Codeに型安全なTypeScriptを生成させる鍵は「型安全を暗黙の了解にしない」ことだ。

  • CLAUDE.mdにstrict設定を転記する — プロジェクト全体で一貫した型ポリシーが定着する
  • tsc --noEmit を必須チェックとして明記する — 型エラーが残ったコードが返ってこなくなる
  • 外部データはZodスキーマから始めるas any を使わない唯一の正道
  • 型エラーログをそのまま渡す — フィードバックループで自動修正が効く
  • Prismaの型を積極的に使わせる — DBスキーマとTypeScript型が自動で同期する

「anyを使わないで」と毎回言うのをやめて、CLAUDE.mdに型ポリシーを書いてしまうのが一番の近道だ。

関連記事: