両面TypeScriptのとき型をどう共有する?
開発環境
- TypeScript 5.4
- orval 7.x
- REST API(GraphQL・モノレポは対象外)
前提
フロントエンドとバックエンドが別リポジトリで、両方 TypeScript を使っているケース。
よくある問題として、API のレスポンス型を両側で二重管理してしまう。
// バックエンド
type User = { id: number; name: string; email: string }
// フロントエンド(コピペして手動で揃える)
type User = { id: number; name: string; email: string }どちらかが変更されたとき、もう片方を忘れるとバグになる。
本題
選択肢の全体像
大きく2つのアプローチがある。
- 手動で共有(コピペ・型定義ファイルを手渡し):最もシンプルだが、ズレが起きやすい
- OpenAPI スキーマから型を生成:スキーマを単一の正にする
この記事では OpenAPI スキーマから型を生成するアプローチ を整理する。
OpenAPI / Swagger ベースの型共有
「OpenAPI スキーマを Single Source of Truth にする」という考え方。
全体の流れ
バックエンド → openapi.yaml → (自動生成) → 型 + API クライアント(フロントエンド)- バックエンドが OpenAPI スキーマ(YAML/JSON)を生成・公開する
- フロントエンドはそのスキーマから型と API クライアントを自動生成する
バックエンド側:スキーマの作り方
大きく2方向ある。
コードファースト(実装 → スキーマ自動生成)
- Express + swagger-jsdoc
- NestJS(デコレータから生成)
- Hono + @hono/zod-openapi
スキーマファースト(スキーマ → コード生成)
- openapi.yaml を先に書いてバリデーションやルーティングを生成
実装とスキーマが乖離しにくいという点で、コードファーストが選ばれやすい。
フロントエンド側:orval で型・クライアントを生成
orval は OpenAPI スキーマから TypeScript の型と API クライアントを自動生成するツール。
設定ファイル(orval.config.ts)を用意したうえで、以下のコマンドだけで生成できる。
npx orval生成されるものの例:
// 自動生成された型
export type User = {
id: number
name: string
email: string
}
// 自動生成された API クライアント
export const getUser = (id: number): Promise<User> =>
fetch(`/users/${id}`).then((res) => res.json())👉 スキーマが更新されたらコマンドを叩くだけで型が追従する。型のズレをコンパイルエラーで検知できるようになる。
設定ファイルの例:
// orval.config.ts
import { defineConfig } from 'orval'
export default defineConfig({
api: {
input: './openapi.yaml',
output: {
target: './src/api/generated.ts',
client: 'fetch', // fetch / axios / react-query など選べる
},
},
})注意点:スキーマとコードの乖離
⚠️ コードファーストの場合、スキーマ再生成を忘れるとフロントエンドの型が古くなる。
実装を変更 → スキーマ再生成を忘れる → フロントエンドの型が古いまま🛠 CI でスキーマ生成 + コミットを強制するのが現実解。差分があればコケるようにしておくと安心。
さいごに
以前は「バックエンドが型を変えたのにフロントエンドに伝え忘れた」という事故を何度か経験しました。ドキュメントやチャットで伝えるしかなく、抜け漏れが防げなかった。
OpenAPI + orval に切り替えてからは、スキーマが変わると npx orval を叩いた瞬間に型エラーが出る。「ツールに検知させる」構造に変えるだけで、認識ズレをゼロにできました。
まずはスキーマを出力するところから始めてみるのがおすすめです。