𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
Claude CodeでTypeScript開発|型安全な実装を自動化する実践ガイド

Claude CodeでTypeScript開発|型安全な実装を自動化する実践ガイド

Claude CodeTypeScript型安全フロントエンド
目次
⚡ 3秒でわかる!この記事のポイント
  • Claude Codeは型定義の自動生成・推論・リファクタリングを高精度で実行できる
  • APIレスポンス型・Zod スキーマ・ジェネリクスの実装を自然言語で指示するだけで完成
  • TypeScript案件はSES市場で需要急増中、月単価75〜95万円が目安

「JavaScriptプロジェクトをTypeScriptに移行したいが、型定義が膨大すぎて手が出ない」「APIのレスポンス型を手動で書くのが面倒」「ジェネリクスの使い方がわからず、any地獄に陥っている」——SES現場でTypeScript開発に携わるエンジニアなら、一度はこうした悩みを抱えたことがあるでしょう。

2026年現在、TypeScriptはフロントエンド・バックエンド問わずSES案件の必須スキルとなっています。React/Next.js案件の90%以上がTypeScriptを採用し、Node.jsバックエンドでもTypeScript化が標準になりました。しかし、型システムの複雑さゆえに開発効率が落ちるケースも少なくありません。

本記事では、Claude Codeを活用してTypeScript開発を劇的に効率化する方法を、型定義の自動生成からリファクタリング、型安全なAPI実装まで体系的に解説します。

TypeScript開発でClaude Codeが威力を発揮する理由

TypeScript開発には「型定義の作成コスト」「型エラーの解消」「ジェネリクスの設計」という3つのボトルネックがあります。Claude Codeは、これらすべてを自然言語の指示で解決できます。

従来のTypeScript開発の課題

課題具体的な問題工数への影響
型定義の手動作成APIレスポンス型を一つずつ定義1エンドポイントあたり30〜60分
型エラーの解消複雑な型推論エラーの原因特定デバッグに数時間かかることも
ジェネリクス設計再利用可能な型の設計に高い専門知識が必要設計に半日〜1日
JS→TS移行既存コードへの段階的な型付けファイルあたり1〜2時間

Claude Codeによる解決

# APIレスポンスから型を自動生成
claude "このJSONレスポンスからTypeScript型定義を生成して。
ネストされたオブジェクトも再帰的に型定義してほしい"

# 型エラーを一括修正
claude "tsc --noEmitで出ているエラーを全て修正して。
any型はできるだけ具体的な型に置き換えて"

# ジェネリクスを使った汎用コンポーネント作成
claude "このフォームコンポーネントをジェネリクスで
汎用化して。任意のフォームスキーマに対応できるようにして"

Claude CodeはTypeScriptの型システムを深く理解しており、Conditional Types、Mapped Types、Template Literal Typesなどの高度な型機能も適切に活用した提案をしてくれます。

実践①:型定義の自動生成

APIレスポンス型の自動生成

SES案件で最も頻繁に行う作業の一つが、APIレスポンスの型定義です。Claude Codeを使えば、JSONサンプルから正確な型定義を瞬時に生成できます。

claude "以下のAPIレスポンスJSONからTypeScript型を生成して。
- null許容のフィールドはoptionalにする
- 日付文字列はstring型でJSDocコメントを付ける
- ネストされたオブジェクトは別の型として分離する

{
  \"id\": 1,
  \"name\": \"田中太郎\",
  \"email\": \"[email protected]\",
  \"profile\": {
    \"bio\": \"エンジニアです\",
    \"avatar_url\": null,
    \"social_links\": [
      {\"platform\": \"twitter\", \"url\": \"https://twitter.com/tanaka\"}
    ]
  },
  \"created_at\": \"2026-01-15T09:00:00Z\",
  \"updated_at\": \"2026-03-10T14:30:00Z\"
}"

Claude Codeが生成する型定義:

/** ソーシャルリンク情報 */
interface SocialLink {
  /** プラットフォーム名 */
  platform: string;
  /** プロフィールURL */
  url: string;
}

/** ユーザープロフィール */
interface UserProfile {
  /** 自己紹介文 */
  bio: string;
  /** アバター画像URL(未設定の場合null) */
  avatar_url?: string | null;
  /** ソーシャルリンク一覧 */
  social_links: SocialLink[];
}

/** ユーザー情報 */
interface User {
  /** ユーザーID */
  id: number;
  /** 氏名 */
  name: string;
  /** メールアドレス */
  email: string;
  /** プロフィール情報 */
  profile: UserProfile;
  /** 作成日時(ISO 8601形式) */
  created_at: string;
  /** 更新日時(ISO 8601形式) */
  updated_at: string;
}

Zodスキーマとの連携

2026年のTypeScript開発では、ランタイムバリデーションライブラリZodとの組み合わせが主流です。Claude Codeは型定義とZodスキーマを同時に生成できます。

claude "上のUser型に対応するZodスキーマも生成して。
z.inferでUser型が導出できるようにして。
バリデーションエラーメッセージは日本語で"
import { z } from 'zod';

const socialLinkSchema = z.object({
  platform: z.string().min(1, 'プラットフォーム名は必須です'),
  url: z.string().url('有効なURLを入力してください'),
});

const userProfileSchema = z.object({
  bio: z.string().max(500, '自己紹介は500文字以内で入力してください'),
  avatar_url: z.string().url().nullable().optional(),
  social_links: z.array(socialLinkSchema),
});

const userSchema = z.object({
  id: z.number().int().positive(),
  name: z.string().min(1, '氏名は必須です'),
  email: z.string().email('有効なメールアドレスを入力してください'),
  profile: userProfileSchema,
  created_at: z.string().datetime(),
  updated_at: z.string().datetime(),
});

// 型をスキーマから導出
type User = z.infer<typeof userSchema>;
type UserProfile = z.infer<typeof userProfileSchema>;
type SocialLink = z.infer<typeof socialLinkSchema>;

export { userSchema, userProfileSchema, socialLinkSchema };
export type { User, UserProfile, SocialLink };

実践②:JavaScript→TypeScript移行

段階的な移行戦略

SES案件では既存のJavaScriptプロジェクトをTypeScriptに移行する作業が頻繁に発生します。Claude Codeを使えば、ファイル単位で安全に移行できます。

# プロジェクト全体の移行計画を立てる
claude "このプロジェクトをTypeScriptに段階的に移行したい。
- まずtsconfig.jsonを設定して
- allowJsをtrueにして共存できるようにして
- 依存関係の少ないユーティリティから移行を開始して
- 移行順序のロードマップを作成して"

tsconfig.jsonの最適設定

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "allowJs": true,
    "checkJs": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

具体的な移行プロンプト

# ユーティリティファイルの移行
claude "src/utils/formatDate.jsをTypeScriptに変換して。
- 引数と戻り値に適切な型を付ける
- any型は使わない
- オーバーロードが必要なら定義する
- JSDocコメントは保持する"

移行前(JavaScript):

// src/utils/formatDate.js
/**
 * 日付をフォーマットする
 * @param {Date|string|number} date - フォーマットする日付
 * @param {string} format - フォーマット文字列
 * @returns {string} フォーマットされた日付文字列
 */
export function formatDate(date, format = 'YYYY-MM-DD') {
  const d = date instanceof Date ? date : new Date(date);
  if (isNaN(d.getTime())) {
    throw new Error('Invalid date');
  }
  
  const map = {
    YYYY: d.getFullYear(),
    MM: String(d.getMonth() + 1).padStart(2, '0'),
    DD: String(d.getDate()).padStart(2, '0'),
    HH: String(d.getHours()).padStart(2, '0'),
    mm: String(d.getMinutes()).padStart(2, '0'),
    ss: String(d.getSeconds()).padStart(2, '0'),
  };
  
  return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => String(map[match]));
}

移行後(TypeScript):

// src/utils/formatDate.ts

/** サポートするフォーマットトークン */
type FormatToken = 'YYYY' | 'MM' | 'DD' | 'HH' | 'mm' | 'ss';

/** 日付として受け入れ可能な型 */
type DateInput = Date | string | number;

/** デフォルトのフォーマット */
const DEFAULT_FORMAT = 'YYYY-MM-DD' as const;

/**
 * 日付をフォーマットする
 * @param date - フォーマットする日付(Date, ISO文字列, UNIXタイムスタンプ)
 * @param format - フォーマット文字列(デフォルト: 'YYYY-MM-DD')
 * @returns フォーマットされた日付文字列
 * @throws {Error} 無効な日付が渡された場合
 *
 * @example
 * formatDate(new Date('2026-03-14'), 'YYYY/MM/DD') // '2026/03/14'
 * formatDate('2026-03-14T09:30:00Z', 'HH:mm') // '09:30'
 */
export function formatDate(
  date: DateInput,
  format: string = DEFAULT_FORMAT
): string {
  const d = date instanceof Date ? date : new Date(date);

  if (Number.isNaN(d.getTime())) {
    throw new Error(`Invalid date: ${String(date)}`);
  }

  const tokenMap: Record<FormatToken, string> = {
    YYYY: String(d.getFullYear()),
    MM: String(d.getMonth() + 1).padStart(2, '0'),
    DD: String(d.getDate()).padStart(2, '0'),
    HH: String(d.getHours()).padStart(2, '0'),
    mm: String(d.getMinutes()).padStart(2, '0'),
    ss: String(d.getSeconds()).padStart(2, '0'),
  };

  const tokenRegex = /YYYY|MM|DD|HH|mm|ss/g;

  return format.replace(tokenRegex, (match) => tokenMap[match as FormatToken]);
}

実践③:型安全なAPI実装

tRPCライクな型安全ルーター

Claude Codeは、エンドツーエンドの型安全性を実現するAPI設計を支援します。

claude "tRPCを使って型安全なAPIルーターを実装して。
- ユーザーCRUD操作
- 入力バリデーションはZod
- エラーハンドリングも型安全に
- Prismaとの連携も含めて"
// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';

const createUserInput = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'member', 'viewer']).default('member'),
});

const updateUserInput = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100).optional(),
  email: z.string().email().optional(),
  role: z.enum(['admin', 'member', 'viewer']).optional(),
});

export const userRouter = router({
  // 一覧取得
  list: publicProcedure
    .input(
      z.object({
        page: z.number().int().positive().default(1),
        limit: z.number().int().min(1).max(100).default(20),
        search: z.string().optional(),
      })
    )
    .query(async ({ input, ctx }) => {
      const { page, limit, search } = input;
      const where = search
        ? {
            OR: [
              { name: { contains: search, mode: 'insensitive' as const } },
              { email: { contains: search, mode: 'insensitive' as const } },
            ],
          }
        : {};

      const [users, total] = await Promise.all([
        ctx.prisma.user.findMany({
          where,
          skip: (page - 1) * limit,
          take: limit,
          orderBy: { createdAt: 'desc' },
        }),
        ctx.prisma.user.count({ where }),
      ]);

      return {
        users,
        pagination: {
          page,
          limit,
          total,
          totalPages: Math.ceil(total / limit),
        },
      };
    }),

  // 個別取得
  getById: publicProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input, ctx }) => {
      const user = await ctx.prisma.user.findUnique({
        where: { id: input.id },
        include: { profile: true },
      });

      if (!user) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: `ユーザーが見つかりません: ${input.id}`,
        });
      }

      return user;
    }),

  // 作成
  create: protectedProcedure
    .input(createUserInput)
    .mutation(async ({ input, ctx }) => {
      const existing = await ctx.prisma.user.findUnique({
        where: { email: input.email },
      });

      if (existing) {
        throw new TRPCError({
          code: 'CONFLICT',
          message: 'このメールアドレスは既に登録されています',
        });
      }

      return ctx.prisma.user.create({ data: input });
    }),

  // 更新
  update: protectedProcedure
    .input(updateUserInput)
    .mutation(async ({ input, ctx }) => {
      const { id, ...data } = input;
      return ctx.prisma.user.update({
        where: { id },
        data,
      });
    }),

  // 削除
  delete: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ input, ctx }) => {
      await ctx.prisma.user.delete({ where: { id: input.id } });
      return { success: true };
    }),
});

フロントエンドでの型安全な呼び出し

// components/UserList.tsx
import { trpc } from '../utils/trpc';

export function UserList() {
  // 戻り値の型が自動的に推論される
  const { data, isLoading, error } = trpc.user.list.useQuery({
    page: 1,
    limit: 20,
  });

  // data.users の型が User[] として正確に推論
  // data.pagination.totalPages も number として推論

  const createMutation = trpc.user.create.useMutation({
    onSuccess: () => {
      // キャッシュの無効化
      utils.user.list.invalidate();
    },
    onError: (error) => {
      // error.message の型が string として推論
      console.error('作成エラー:', error.message);
    },
  });

  const handleCreate = async () => {
    // 型安全:name, email, role以外のプロパティは渡せない
    await createMutation.mutateAsync({
      name: '新しいユーザー',
      email: '[email protected]',
      role: 'member', // 'admin' | 'member' | 'viewer' のみ許可
    });
  };

  if (isLoading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error.message}</div>;

  return (
    <div>
      {data?.users.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
        </div>
      ))}
    </div>
  );
}

実践④:高度な型テクニック

Conditional Typesの活用

Claude Codeは複雑なConditional Typesも的確に生成します。

claude "APIレスポンスの型を、成功時とエラー時で
分岐する型を作って。ディスクリミネーテッドユニオンで。
成功時はdataフィールド、エラー時はerrorフィールドを持つ"
// 型安全なAPIレスポンス
type ApiResponse<T> =
  | { success: true; data: T; error?: never }
  | { success: false; data?: never; error: ApiError };

interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

// 使用例
async function fetchUser(id: string): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return {
        success: false,
        error: {
          code: 'FETCH_ERROR',
          message: `HTTP ${response.status}: ${response.statusText}`,
        },
      };
    }
    const data = await response.json();
    return { success: true, data };
  } catch (e) {
    return {
      success: false,
      error: {
        code: 'NETWORK_ERROR',
        message: e instanceof Error ? e.message : '不明なエラー',
      },
    };
  }
}

// 型ガードが自動的に効く
const result = await fetchUser('123');
if (result.success) {
  // result.data は User 型として推論
  console.log(result.data.name);
} else {
  // result.error は ApiError 型として推論
  console.error(result.error.message);
}

Mapped Typesでフォーム状態管理

claude "フォームの状態管理を型安全にしたい。
各フィールドのvalue, error, touched状態を
元の型から自動生成する Mapped Type を作って"
// フォームフィールドの状態
interface FieldState<T> {
  value: T;
  error: string | null;
  touched: boolean;
  dirty: boolean;
}

// 型から自動的にフォーム状態を生成
type FormState<T extends Record<string, unknown>> = {
  [K in keyof T]: FieldState<T[K]>;
};

// フォームのアクション
type FormAction<T extends Record<string, unknown>> =
  | { type: 'SET_VALUE'; field: keyof T; value: T[keyof T] }
  | { type: 'SET_ERROR'; field: keyof T; error: string | null }
  | { type: 'SET_TOUCHED'; field: keyof T }
  | { type: 'RESET' }
  | { type: 'VALIDATE' };

// 使用例
interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

// FormState<LoginForm> は以下と同等:
// {
//   email: FieldState<string>;
//   password: FieldState<string>;
//   rememberMe: FieldState<boolean>;
// }

function useTypedForm<T extends Record<string, unknown>>(
  initialValues: T,
  validate: (values: T) => Partial<Record<keyof T, string>>
) {
  const initialState: FormState<T> = Object.fromEntries(
    Object.entries(initialValues).map(([key, value]) => [
      key,
      { value, error: null, touched: false, dirty: false },
    ])
  ) as FormState<T>;

  // ... reducer implementation
  return { state: initialState };
}

実践⑤:TypeScript×Claude Codeのワークフロー最適化

CLAUDE.mdでのTypeScript設定

プロジェクトルートのCLAUDE.mdにTypeScript固有のルールを記載することで、Claude Codeの出力品質を大幅に向上させます。

# TypeScriptルール

## 型定義
- any型の使用禁止(unknown + 型ガードを使う)
- as アサーションよりも型ガード関数を優先
- 戻り値の型は明示的に書く(型推論に頼りすぎない)
- interfaceよりtypeを優先(拡張が必要な場合のみinterface)

## コーディング規約
- strict: trueを前提とする
- noUncheckedIndexedAccess: trueを前提とする
- enumは使わない(const objectsまたはunion typeを使う)
- namespace は使わない(ES modules を使う)

## インポート
- type-only importを使う: import type { Foo } from './foo'
- バレルファイル(index.ts)は使わない(ビルド性能に影響)

## エラーハンドリング
- catchしたエラーはunknownとして扱う
- カスタムエラークラスを使って型安全にする

型チェックの自動実行

# tscエラーを検出して一括修正
claude "tsc --noEmitを実行して、出ているエラーを全て修正して。
修正方針:
1. any → 具体的な型に変更
2. 型の不一致 → 正しい型に修正
3. undefinedの可能性 → オプショナルチェーンまたは型ガード追加
4. 未使用変数 → アンダースコアプレフィックスを付けるか削除"

SES現場でのTypeScriptスキルの市場価値

2026年のTypeScript案件動向

TypeScript開発スキルとSES案件の関係を示すインフォグラフィック

TypeScriptのスキルレベルによって、SES案件の単価は大きく変わります。

スキルレベルできること月単価目安
基礎型アノテーション、基本的な型定義60〜70万円
中級ジェネリクス、Utility Types活用70〜85万円
上級Conditional Types、型レベルプログラミング85〜95万円
エキスパート型安全なフレームワーク設計、DX改善95万円以上

需要の高いTypeScript関連スキル

  1. React + TypeScript: コンポーネントの型安全な設計
  2. Next.js App Router + TypeScript: サーバーコンポーネント型
  3. tRPC / GraphQL Code Generator: エンドツーエンド型安全
  4. Prisma: 型安全なデータベースアクセス
  5. Zod / Valibot: ランタイムバリデーション

実践⑥:テスト自動生成

型を活用したテスト

claude "このuserRouter.tsに対するユニットテストを生成して。
- vitestを使う
- 各プロシージャの正常系・異常系をカバー
- モック型もPrismaの型から自動生成
- テストヘルパーは型安全にする"
// __tests__/user.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createCaller } from '../server/routers/_app';
import type { PrismaClient } from '@prisma/client';
import { TRPCError } from '@trpc/server';

// 型安全なモック
const mockPrisma = {
  user: {
    findMany: vi.fn(),
    findUnique: vi.fn(),
    create: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
    count: vi.fn(),
  },
} satisfies { user: Partial<PrismaClient['user']> };

const caller = createCaller({
  prisma: mockPrisma as unknown as PrismaClient,
  session: { userId: 'test-user-id' },
});

describe('userRouter', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('list', () => {
    it('ページネーション付きでユーザー一覧を返す', async () => {
      const mockUsers = [
        { id: '1', name: 'テスト太郎', email: '[email protected]' },
      ];
      mockPrisma.user.findMany.mockResolvedValue(mockUsers);
      mockPrisma.user.count.mockResolvedValue(1);

      const result = await caller.user.list({ page: 1, limit: 20 });

      expect(result.users).toEqual(mockUsers);
      expect(result.pagination.total).toBe(1);
      expect(result.pagination.totalPages).toBe(1);
    });

    it('検索クエリでフィルタリングする', async () => {
      mockPrisma.user.findMany.mockResolvedValue([]);
      mockPrisma.user.count.mockResolvedValue(0);

      await caller.user.list({ page: 1, limit: 20, search: 'テスト' });

      expect(mockPrisma.user.findMany).toHaveBeenCalledWith(
        expect.objectContaining({
          where: expect.objectContaining({
            OR: expect.arrayContaining([
              expect.objectContaining({
                name: expect.objectContaining({ contains: 'テスト' }),
              }),
            ]),
          }),
        })
      );
    });
  });

  describe('create', () => {
    it('新しいユーザーを作成する', async () => {
      const input = {
        name: '新規ユーザー',
        email: '[email protected]',
        role: 'member' as const,
      };
      mockPrisma.user.findUnique.mockResolvedValue(null);
      mockPrisma.user.create.mockResolvedValue({ id: '2', ...input });

      const result = await caller.user.create(input);

      expect(result.name).toBe(input.name);
      expect(mockPrisma.user.create).toHaveBeenCalledWith({
        data: input,
      });
    });

    it('重複メールアドレスでエラーを返す', async () => {
      mockPrisma.user.findUnique.mockResolvedValue({
        id: '1',
        email: '[email protected]',
      });

      await expect(
        caller.user.create({
          name: 'テスト',
          email: '[email protected]',
        })
      ).rejects.toThrow(TRPCError);
    });
  });
});

まとめ:Claude Code × TypeScriptで開発速度を3倍に

Claude Codeを活用したTypeScript開発のポイントをおさらいしましょう。

活用シーン効果時間短縮
型定義の自動生成APIレスポンス型を即座に生成最大80%短縮
JS→TS移行ファイル単位で安全に型付け最大70%短縮
型安全なAPI実装tRPC/Zodの定型パターン生成最大60%短縮
高度な型テクニックConditional/Mapped Types生成設計時間を大幅短縮
テスト自動生成型情報を活用した網羅的テスト最大75%短縮

TypeScriptスキル × AI活用は、2026年のSES市場で最も評価される組み合わせの一つです。Claude Codeを日々の開発に取り入れることで、型安全なコードを効率的に書けるようになり、より高単価な案件にチャレンジできるようになります。


📚 Claude Code 完全攻略シリーズの他の記事もチェック!

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修