Claude CodeでTypeScript開発を速く安全にする実践Tips
strict設定、Zod、Union、型テストでClaude CodeのTypeScript開発を安全に速くする。
Claude CodeにTypeScript実装を任せると、フォーム、API、テストの初速はかなり上がります。
ただし初心者ほど「動いたからOK」と判断しやすく、any、広すぎる string、検証されていないJSONが後から効いてきます。
速くするほど、最初に型のガードレールを置く必要があります。
この記事では、Claude Codeにコードを書かせる前に人間が決めるべきTypeScriptの型ルールを、コピペで動く例に絞って整理します。
strict は「怪しい型を見逃さない設定」、ドメイン型は「業務ルールをコードに写した型」、判別可能Unionは「状態ごとに必要な値を分ける型」、ランタイム検証は「実行中に値の形を確認する処理」です。
難しい型芸ではなく、AIが作ったコードを実務で壊れにくくするための足場として考えてください。
まず型の地図を渡す
Claude Codeへ最初に渡すべきなのは、長いプロンプトよりも小さな型の地図です。 要件、設定、外部入力、状態、テストを分けると、生成されたコードのレビューもしやすくなります。
flowchart TD
A["要件: 何を作るか"] --> B["tsconfig: strictな制約"]
B --> C["ドメイン型: PlanやAccount"]
C --> D["外部入力: unknownから検証"]
D --> E["状態: 判別可能Union"]
E --> F["型テスト: expectTypeOf / tsd"]
F --> G["Claude Codeへ実装と修正を依頼"]
公式情報は必ず一次情報を見ます。
TypeScriptの設定は TSConfigのstrict、noUncheckedIndexedAccess、exactOptionalPropertyTypes を確認します。
型の絞り込みは Narrowing、再利用する型は Generics、Pick や Partial は Utility Types、satisfies は TypeScript 4.9のリリースノート が基準です。
実行時検証は Zod公式ドキュメント も併読してください。
関連する基礎は、TypeScriptユーティリティ型入門、TypeScriptジェネリクス実践ガイド、Zodバリデーション実装 と合わせるとつながります。
strictなtsconfigを最初に作る
「TypeScriptで作って」とだけ頼むと、Claude Codeは動くことを優先し、曖昧な型で通すことがあります。
最初に tsconfig.json を固定してから依頼すると、AIの出力もその制約の中に収まりやすくなります。
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts"]
}
Claude Codeへの依頼文も、目的だけでなく禁止事項を入れます。
このリポジトリはstrict TypeScript前提です。
anyは禁止し、外部入力はunknownとして受けてZodで検証してください。
switchでUnionを扱う場合はneverで網羅性チェックを入れてください。
実装後に npx tsc --noEmit を通してください。
noUncheckedIndexedAccess は、配列やオブジェクトから値を取るときに undefined の可能性を残します。
少し面倒ですが、設定ファイルのキー抜け、APIレスポンスの空配列、CMSの翻訳漏れを早く見つけられます。
exactOptionalPropertyTypes は、任意プロパティと undefined を混同しにくくします。
初心者には厳しく見えますが、Claude Codeが雑に値を詰めたときに止めてくれるので、むしろ助けになります。
ユースケース1: SaaSのプランをドメイン型にする
ドメイン型は、業務の言葉をTypeScriptに落としたものです。 料金プラン、権限、請求状態のようなルールは、画面を作る前に型にします。
export type Plan = "free" | "pro" | "enterprise";
export type Account = {
id: string;
email: string;
plan: Plan;
seats: number;
trialEndsAt: string | null;
};
export type CreateAccountInput = {
email: string;
plan: Exclude<Plan, "enterprise">;
seats?: number;
};
export type UpdateAccountInput = Partial<
Pick<Account, "email" | "plan" | "seats" | "trialEndsAt">
>;
Exclude はUnion型から一部を取り除く道具です。
この例では、新規作成画面から enterprise を直接選べないようにしています。
Partial はすべてのプロパティを任意にする道具です。
更新APIでは一部だけ送ることが多いので便利ですが、新規作成まで Partial にすると必須項目が消える点に注意します。
ユースケース2: APIレスポンスをunknownから検証する
TypeScriptの型はビルド時の仕組みです。
外部API、フォーム、Cookie、localStorage、CSV、AI出力は、実行時には壊れた値を持つ可能性があります。
そのため境界では any ではなく unknown で受け、Zodなどで検証してから使います。
npm install zod
import { z } from "zod";
const AccountSchema = z.object({
id: z.string().min(1),
email: z.string().email(),
plan: z.enum(["free", "pro", "enterprise"]),
seats: z.number().int().positive(),
trialEndsAt: z.string().datetime().nullable()
});
type Account = z.infer<typeof AccountSchema>;
export function parseAccountResponse(json: unknown): Account {
return AccountSchema.parse(json);
}
const raw = JSON.parse(`{
"id": "a1",
"email": "masa@example.com",
"plan": "pro",
"seats": 3,
"trialEndsAt": null
}`);
console.log(parseAccountResponse(raw).email);
unknown は「まだ何かわからない値」です。
any と違い、そのままプロパティを読めません。
Claude Codeには「外部入力はunknownで受け、Zod通過後だけAccountとして扱う」と明記してください。
ユースケース3: 決済状態を判別可能Unionで閉じる
決済、アップロード、フォーム送信、バックグラウンドジョブは状態を持ちます。
status: string と書くと、存在しない状態が紛れます。
判別可能Unionを使うと、状態ごとに必要な値を強制できます。
type PaymentResult =
| { status: "pending"; invoiceId: string }
| { status: "paid"; invoiceId: string; paidAt: string }
| { status: "failed"; invoiceId: string; reason: string };
export function renderPaymentMessage(result: PaymentResult): string {
switch (result.status) {
case "pending":
return `Invoice ${result.invoiceId} is waiting for payment.`;
case "paid":
return `Invoice ${result.invoiceId} was paid at ${result.paidAt}.`;
case "failed":
return `Invoice ${result.invoiceId} failed: ${result.reason}.`;
default: {
const exhaustive: never = result;
return exhaustive;
}
}
}
console.log(renderPaymentMessage({
status: "paid",
invoiceId: "inv_001",
paidAt: "2026-06-03T09:00:00.000Z"
}));
never は「ここには到達しないはず」という型です。
あとから status: "refunded" を追加して switch に処理を書き忘れると、TypeScriptがエラーで教えてくれます。
Claude Codeに修正を頼むときは「neverの網羅性エラーが消えるまで分岐を直して」と指示できます。
ユースケース4: genericsとsatisfiesで設定を守る
ジェネリクスは、型をあとから差し込める部品です。
次の groupBy は、アカウント、記事、注文など、いろいろな配列に使えます。
noUncheckedIndexedAccess 前提でも扱いやすいように、戻り値は Partial<Record<K, T[]>> にしています。
export function groupBy<T, K extends PropertyKey>(
items: readonly T[],
getKey: (item: T) => K
): Partial<Record<K, T[]>> {
const grouped: Partial<Record<K, T[]>> = {};
for (const item of items) {
const key = getKey(item);
const bucket = grouped[key] ?? [];
bucket.push(item);
grouped[key] = bucket;
}
return grouped;
}
const accounts = [
{ id: "a1", plan: "free" },
{ id: "a2", plan: "pro" },
{ id: "a3", plan: "pro" }
] as const;
const byPlan = groupBy(accounts, (account) => account.plan);
const proAccounts = byPlan.pro ?? [];
console.log(proAccounts.map((account) => account.id));
設定オブジェクトでは satisfies が役立ちます。
型に合うかを確認しつつ、"GET" や "/accounts" のような具体的な値を残せます。
type ApiRoute = {
method: "GET" | "POST" | "PATCH" | "DELETE";
path: `/${string}`;
auth: boolean;
};
const routes = {
listAccounts: { method: "GET", path: "/accounts", auth: true },
createAccount: { method: "POST", path: "/accounts", auth: true },
healthCheck: { method: "GET", path: "/health", auth: false }
} as const satisfies Record<string, ApiRoute>;
type RouteName = keyof typeof routes;
export function getRoute(name: RouteName) {
return routes[name];
}
Claude Codeには「as SomeType で逃げず、設定表は satisfies で検査して」と書くと、ルート、Feature Flag、価格表、デザイントークンの品質が上がります。
型レベルテストを置く
型は画面に出ないので、レビューだけでは広がりに気づきにくいです。
公開する型にはVitestの expectTypeOf やtsd風のテストを置きます。
npm install -D vitest tsd
import { expectTypeOf, test } from "vitest";
type CreateAccountInput = {
email: string;
plan: "free" | "pro";
seats?: number;
};
test("CreateAccountInput keeps the public API narrow", () => {
expectTypeOf<CreateAccountInput>().toMatchTypeOf<{
email: string;
plan: "free" | "pro";
seats?: number;
}>();
});
tsdでは「エラーになってほしい呼び出し」も残せます。
import { expectError, expectType } from "tsd";
import { renderPaymentMessage } from "./payment";
expectType<string>(renderPaymentMessage({
status: "pending",
invoiceId: "inv_001"
}));
expectError(renderPaymentMessage({
status: "refunded",
invoiceId: "inv_001"
}));
実務で効く使い分け
| 場面 | 型で固定するもの | Claude Codeに任せやすいもの |
|---|---|---|
| SaaSの料金プラン | Plan、請求状態、権限 | 画面分岐、フォーム、エラーメッセージ |
| 管理画面のAPI連携 | Zodスキーマ、レスポンス型 | fetch関数、テーブル表示、ローディング処理 |
| 記事CMS | slug、言語、公開状態、OGP画像 | MDX下書き、一覧ページ、バリデーション修正 |
| 問い合わせフォーム | 入力スキーマ、送信結果Union | UI、送信処理、Vitest追加 |
MasaがClaudeCodeLabの記事生成で失敗したのは、lang: string のまま多言語記事を作り、存在しないロケールURLを混ぜたことです。
"ja" | "en" | "zh" | "ko" | "es" | "fr" | "de" | "pt" | "hi" | "id" のように閉じたUnionへ変えるだけで、Claude Codeの修正提案もかなり安定しました。
具体的な落とし穴
| 落とし穴 | 何が起きるか | 対策 |
|---|---|---|
APIレスポンスを any で受ける | 壊れたJSONでもコンパイルが通る | unknown とZodで検証する |
status: string と書く | 存在しない状態が混ざる | 判別可能Unionにする |
as User を多用する | 型エラーを消しただけになる | satisfies、型ガード、スキーマを使う |
Partial<T> を新規作成に使う | 必須項目まで任意になる | 作成用と更新用の型を分ける |
| 型テストがない | リファクタリングで公開型が広がる | expectTypeOf やtsdを追加する |
| Claude Codeに丸投げする | 設計意図が薄いコードになる | CLAUDE.md に型ルールを書く |
特に Omit は型からキーを消すだけで、実行時のオブジェクトから秘密情報を削除しません。
ログやAPIレスポンスから passwordHash を消す処理は、別途コードで書く必要があります。
どこから導入すると失敗しにくいか
既存プロジェクトでいきなり全ファイルをstrict化すると、エラー数が多すぎてClaude Codeも人間も判断しづらくなります。
最初の一歩は、外部入力に触れる小さな境界を1つ選ぶことです。
たとえば問い合わせフォーム、料金プラン変更API、CSV取り込み、Webhook受信のように「壊れた値が入ってきそうな場所」だけを対象にします。
そこにZodスキーマ、判別可能Union、型テストを入れ、npx tsc --noEmit と既存テストを通します。
次に見るのは、Claude Codeがよく触る共通型です。
User、Account、Article、Plan のような型が広すぎると、どの記事生成、管理画面、決済処理でも同じ間違いが増えます。
共通型を一度だけ狭くして、変更理由を CLAUDE.md に残しておくと、次のセッションでも同じ設計意図を引き継げます。
この順番なら、リポジトリ全体を一気に直すより差分が小さく、レビューもしやすいです。
大事なのは、型エラーをゼロにすることだけを目的にしないことです。 型は、AIに「ここから先は推測で進まないで」と伝える境界線です。 修正依頼では「エラーを消して」ではなく、「外部入力、状態分岐、公開型の3点で安全になるように直して」と書くと、生成される差分の質が上がります。
CLAUDE.mdに書くTypeScriptルール
## TypeScript rules
- Use strict TypeScript.
- Do not introduce `any`. Use `unknown` at external boundaries.
- Prefer discriminated unions for states.
- Prefer `satisfies` over broad type assertions.
- Derive API types from Zod schemas when runtime data is involved.
- Add Vitest or tsd style type checks for exported helper types.
- Run `npx tsc --noEmit` before reporting completion.
短いルールでも、Claude Codeの出力は変わります。 レビューを頼むときは、次のように「型のリスク」を先に見てもらいます。
このTypeScript差分を本番リスク目線でレビューしてください。
新しいany、広すぎるas、未検証JSON境界を指摘してください。
Unionの状態がすべて処理されているか確認してください。
Zod schemaとexported typeがズレていないか確認してください。
無関係なファイルは書き換えないでください。
プロダクトと研修への導線
個人でまず型ルールを整えたい場合は、Claude Code用のチェックリストやテンプレートを プロダクト一覧 にまとめています。
チームで既存リポジトリをstrict化し、Zod境界、型テスト、CLAUDE.md まで一緒に整えるなら、Claude Code研修・相談 で実プロジェクト前提の進め方を相談できます。
型の標準は、長いプロンプト集より長く効きます。 Claude Codeに速さを任せるほど、人間は「どこで止めるか」を型で決める必要があります。 最初から完璧な型設計を目指す必要はありません。 新しい画面を作るたびに、外部入力、状態、保存前データの3か所だけを見直すだけでも、生成コードの事故はかなり減ります。
実際に試した結果
この記事のサンプルは、strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes を前提にしても破綻しにくい形へ寄せました。
実際の案件では、APIレスポンスを any で受けていた箇所を unknown とZodに変え、Claude CodeにUnion分岐とVitestの型チェックを追加させたところ、レビュー前に「未処理の状態」と「存在しないプロパティ参照」を検出できました。
一番効いたのは、実装依頼の前に tsconfig、ドメイン型、外部入力の扱いを渡したことです。
AIに速度を出させるほど、TypeScriptは邪魔なブレーキではなく、事故を減らすための操縦席になります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Code権限セーフティラダー: 初心者がallowを広げる順番
Claude Codeの権限をread-onlyからbuild、限定編集、deploy確認まで段階的に広げる安全な運用手順。
Claude Code Small PR Proof Pack: 小さなPRをレビュー可能にする証拠セット
Claude Codeの小さなPRに、差分・検証・公開URL・CTA・rollbackを添える実務チェックリスト。
Claude Codeのコミット前レビューゲート: 差分、テスト、CTAをまとめて止める型
Claude Codeでcommit前に差分をレビューする実践手順。build、公開URL、CTA、Gumroadリンク、未翻訳本文を検知します。