Claude Media
Claude Codeでopenapi-typescriptを使う — 型エラーで呼び出し側の修正箇所を洗う

Claude Codeでopenapi-typescriptを使う — 型エラーで呼び出し側の修正箇所を洗う

openapi-typescriptで生成した型とopenapi-fetchの組み合わせを、Claude Codeに任せる設定です。生成ファイルはdenyで守り、仕様の更新後はhookで再生成してtscの型エラーを修正リストにします。

openapi-typescriptは、OpenAPI 3.0と3.1のスキーマをTypeScriptの型に変換するツールです。生成物はランタイムを持たない型定義だけで、実際のHTTP呼び出しはopenapi-fetchが受け持ちます。Claude Codeに任せるときの勘所は、この「型が正で、呼び出しコードはその型に従う」という向きを崩させないことです。

型を作るだけの方式なので、Java系のジェネレーターが出力するクライアントコード一式とは扱いが違います。クライアントごと生成する構成はClaude CodeでOpenAPI Generatorを使うにあります。ここでは型だけを出す方式に絞り、次の3点を設定します。

  1. 生成された.d.tsをClaudeに手編集させない(permissions.deny)
  2. 仕様ファイルを編集した直後に型を再生成し、tsc --noEmitの結果をClaudeへ返す(PostToolUse hook)
  3. 型エラーをasやanyで黙らせず、呼び出し側を直させる(CLAUDE.mdの規約)

型だけを出す構成と、Claudeが触る場所

構成は3層です。仕様(openapi/api.yaml)が正、生成物(src/lib/api/v1.d.ts)はその写し、src/の呼び出しコードは生成物に従う側になります。

openapi-typescriptのCLIは、入力スキーマと--output(-o)を指定して実行します。ローカルのファイルも、URLで公開されたスキーマも読めます。

npm i -D openapi-typescript typescript
npm i openapi-fetch
npx openapi-typescript ./openapi/api.yaml -o ./src/lib/api/v1.d.ts

呼び出し側は、生成されたpaths型をopenapi-fetchのクライアントに渡します。ドキュメントの例に沿った形は次のとおりです。

import createClient from "openapi-fetch";
import type { paths } from "./v1"; // openapi-typescriptの生成物
 
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
 
const { data, error } = await client.GET("/blogposts/{post_id}", {
  params: { path: { post_id: "123" } },
});

パスは"/blogposts/{post_id}"のようにスキーマの表記どおりの文字列で渡し、パラメータはparamsにまとめます。必須パラメータの欠落や型の不一致はコンパイル時の型エラーになります。dataは2xxのとき、errorは4xx・5xxのときだけ値を持ち、どちらも型がつきます。

ここからClaudeの持ち場が決まります。

種類パスの例Claudeの扱い
仕様パスの例openapi/api.yamlClaudeの扱い編集してよい。変更の起点
生成物パスの例src/lib/api/v1.d.tsClaudeの扱い読めるが書けない
呼び出しコードパスの例src/**/*.tsClaudeの扱い型に従って直す
生成コマンドパスの例package.jsonのscriptsClaudeの扱い実行してよい

openapi-fetchは「tsc --noEmitを型チェックの手段にする」ことを前提にしています。ドキュメントでも、リンターやビルドコマンドではなくTypeScriptコンパイラー自身で検査するよう勧めています。この前提が、後半のhook設計の根拠になります。

生成物への手編集をdenyで止める

型エラーが出たとき、最短の「解決」は生成されたv1.d.tsを書き換えることです。次の再生成で消える変更なので、止めておきます。.claude/settings.jsonに拒否ルールを入れます。

{
  "permissions": {
    "deny": [
      "Edit(/src/lib/api/v1.d.ts)"
    ]
  }
}

先頭が/のパターンは、プロジェクト設定ではプロジェクトルートからの相対パスとして扱われます。ファイルの編集権限はEdit(path)とRead(path)のルールだけで判定され、Write(...)で書いたルールは受理されても一致しません。Readをdenyにすると型定義そのものが読めなくなるので、生成物にはEditのdenyを選びます。

再生成のコマンドは、denyを入れても動きます。拒否ルールが効くのは組み込みのファイルツールと、Bashの中のcat・sed・teeなどの認識されたファイルコマンド、リダイレクトの出力先までです。npx openapi-typescriptが自分でファイルを開いて書く動作には及びません。

同じ「生成物を触らせないdeny」はGraphQLでも使います。スキーマ駆動の型生成を比べたいときはGraphQL Codegenの設定が参考になります。

仕様を編集したら型を再生成し、tscの結果を返す

denyだけでは、Claudeがapi.yamlを直したあとにv1.d.tsが古いままです。ここでPostToolUse hookを使います。PostToolUseはツール呼び出しが成功したあとに発火し、Edit|Writeをマッチャーにするとファイル編集の直後だけ動きます。仕組みの全体像はPostToolUse hookの解説にあります。

hookはJSONを標準入力で受け取ります。編集されたパスはtool_input.file_pathにあります。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/openapi-types.sh"
          }
        ]
      }
    ]
  }
}

スクリプトは、仕様ファイルの編集だけを対象にします。再生成に成功したらtsc --noEmitを走らせ、型エラーが残っていればstderrへ出して終了コード2で返します。

#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
 
# 仕様ファイル以外の編集では何もしない
case "$FILE" in
  */openapi/*.yaml|*/openapi/*.json) ;;
  *) exit 0 ;;
esac
 
cd "$CLAUDE_PROJECT_DIR" || exit 0
 
if ! OUT=$(npx openapi-typescript ./openapi/api.yaml \
    -o ./src/lib/api/v1.d.ts 2>&1); then
  echo "型の再生成に失敗しました。仕様の記述を確認してください:" >&2
  echo "$OUT" | head -30 >&2
  exit 2
fi
 
if ! TSC=$(npx tsc --noEmit 2>&1); then
  echo "仕様の変更により型エラーが出ています。呼び出し側を直してください:" >&2
  echo "$TSC" | head -40 >&2
  exit 2
fi
exit 0

実行権限を付けておきます。権限がないとhookは動きません。

chmod +x .claude/hooks/openapi-types.sh

終了コード2の意味は、イベントごとに違います。PostToolUseではツールがすでに実行済みなので編集は取り消せませんが、stderrの内容がClaudeに渡されます。この性質が、ここでは都合よく働きます。仕様の編集は成功し、その結果として壊れた呼び出し側の一覧が、そのままClaudeの次の作業リストになるからです。

openapi-typescriptのCLIはRedoclyで仕様を検証するため、仕様自体が壊れていれば再生成の段階で失敗します。この場合もstderrの要約がClaudeに戻り、仕様の書き直しに進めます。

手順

仕様変更から修正完了までの流れ

  1. 1

    仕様を編集する

    Claudeがopenapi/api.yamlのレスポンスやパラメータを書き換えます。

  2. 2

    hookが型を再生成する

    v1.d.tsが新しい仕様で書き直され、paths型が更新されます。

  3. 3

    tscが壊れた呼び出しを列挙する

    型が合わなくなったファイルと行が、エラーとしてClaudeに戻ります。

  4. 4

    Claudeが呼び出し側を直す

    エラーが空になるまで、src/側だけを編集します。

型エラーを黙らせない規約をCLAUDE.mdに置く

hookが返すのは事実だけです。エラーをどう片付けるかは、Claudeの判断に委ねられます。放っておくと、asによる型断言や@ts-ignoreでエラーを消す選択肢が混ざります。openapi-fetchの利点は「asやanyで隠れるバグが消える」ことなので、規約で戻り道を塞ぎます。

## API クライアント
- 型の正は openapi/api.yaml。src/lib/api/v1.d.ts は生成物で編集しない
- 仕様を変えたら hook が型を再生成する。出た tsc のエラーは呼び出し側で直す
- 型エラーを as / any / @ts-ignore で消さない
- 呼び出しは client.GET("/blogposts/{post_id}", ...) のように
  パスを文字列リテラルで書く(動的に組み立てない)

最後の1行には理由があります。openapi-fetchはURLの文字列から型を推論するので、[...pathParts].join("/")のように実行時に組み立てたパスでは型が効かなくなります。ドキュメントも静的な文字列を使うよう明記しています。

tsconfigと戻り値の取り扱い

型の効きを強める設定が2つあります。

  • tsconfig.jsonのnoUncheckedIndexedAccess: true: openapi-typescript・openapi-fetchの両ドキュメントが、型安全性を高める設定として勧めています
  • moduleResolutionはBundlerかNodeNext: 生成された型を正しく読み込むための設定です

戻り値では、dataとerrorの片方がundefinedになります。dataをそのまま使うコードは型エラーになるので、Claudeが書いた呼び出しにはエラー分岐が自然に入ります。分岐を省くためにdata!と書かせないことも、規約に足せます。

生成フラグで型の厳しさを選ぶ

Claudeが書く呼び出しコードの安全度は、生成される型の厳しさにも左右されます。CLIの既定値は、フラグで変えられます。Claudeに再生成コマンドを実行させるなら、フラグはscriptsに固定しておき、毎回Claudeが選ぶ形にはしません。

フラグ既定効果
--immutable既定false効果プロパティと配列をreadonlyにする。レスポンスの書き換えをエラーにできる
--read-write-markers既定false効果readOnly・writeOnlyのプロパティに印をつけ、openapi-fetchが書き込み時にidなどの読み取り専用項目を弾く
--properties-required-by-default既定false効果requiredのないスキーマの全プロパティを必須として扱う
--exclude-deprecated既定false効果非推奨のフィールドを型から外す

--exclude-deprecatedは、非推奨フィールドの使用をコンパイルエラーに変えます。仕様側でフィールドをdeprecatedにすると、使っている呼び出しが洗い出されるので、このあとの型エラーの流れにそのまま乗ります。--read-write-markersを使うと、idのような読み取り専用項目をPOSTの本文に入れた呼び出しも型で検出できます。ただし既存の呼び出しが多いと一度に大量のエラーが出るため、段階的に有効化します。

CIでは--checkで生成物のずれを検出する

hookはClaudeのセッションの中でしか動きません。人が仕様だけを直してコミットした場合に備え、CIにも検査を置きます。CLIの--checkフラグは、生成済みの型が最新かどうかを確かめます。

{
  "scripts": {
    "api:gen": "openapi-typescript ./openapi/api.yaml -o ./src/lib/api/v1.d.ts",
    "api:check": "openapi-typescript ./openapi/api.yaml -o ./src/lib/api/v1.d.ts --check",
    "typecheck": "tsc --noEmit"
  }
}

api:checkとtypecheckを続けて回せば、「仕様と生成物のずれ」と「生成物と呼び出し側のずれ」を別々に検出できます。フラグの説明は「生成済みの型が最新かを確認する」とだけ書かれているため、出力先の指定との組み合わせは手元で一度実行して挙動を確かめてから組み込みます。

よくあるつまずき

落とし穴

フックがtscで長く止まる

大きなプロジェクトではtsc --noEmitが仕様の編集のたびに走り、待ち時間が目立ちます。ここで示したスクリプトは、仕様以外のパスでは即座にexit 0するので、通常のコード編集では走りません。それでも重いときは、tscを回す対象を仕様に依存するパッケージのtsconfigに絞ります。

  • 生成物をgit管理するかどうか: 管理しない場合、CIは必ずapi:genを先に回す必要があります。管理する場合はdenyとCIの--checkが二重の守りになります
  • スキーマがURL配布のとき: 認証が要るスキーマは、Redocly設定のresolve.http.headersで環境変数のキーを渡します。キーの値そのものをsettings.jsonやCLAUDE.mdに書かないようにします
  • hookが発火しない: Edit|Writeの綴りとスクリプトの実行権限を先に確認します。/hooksで設定が読み込まれているかも見られます

まとめ

openapi-typescriptとopenapi-fetchの組み合わせは、型が壊れる場所を呼び出し側のコンパイルエラーに集約します。Claude Codeでは、生成物をdenyで固定し、仕様の編集直後にhookで再生成とtscを回し、stderrで返すことで、その性質を修正の自動化に使えます。人が仕様を直した場合の抜けはCIの--checkが拾うので、hookとCIで役割を分けておけば足ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. openapi-ts.dev/introduction
  2. openapi-ts.dev/cli
  3. openapi-ts.dev/openapi-fetch
  4. code.claude.com/docs/en/hooks-guide
  5. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn