Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TypeSpec で繋ぐ複数プロダクトの型安全

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on.

TypeSpec で繋ぐ複数プロダクトの型安全

2つのプロダクトをTypeScriptでフルスタック構築する中で、TypeSpecを「型契約」の基盤として運用し、スキーマ管理とサービス連携を自動化した事例を紹介します。

チーム内の開発では、TypeSpecからバリデーション用のZod、APIクライアント、MSWモックを一貫して自動生成するパイプラインを構築しました。これにより手動の同期作業を完全に排除し、フロントエンドとバックエンドの実装を常に型で一致させています。さらにこの仕組みをチーム間の連携にも広げ、一方のサービスが定義したAPIやWebhookのスキーマをnpmパッケージとして共有する体制を整えました。スキーマをドキュメントではなく物理的な「型」として直接参照し合うことで、サービスを跨ぐインターフェースの変更をコンパイルタイムで確実に検知できます。

複数チームが関わる開発において、いかに「型」を境界のガードレールとして機能させ、認識のズレを未然に防ぐか。その具体的な構成と運用の知見を共有します。

Avatar for Mitsui

Mitsui

May 22, 2026

More Decks by Mitsui

Other Decks in Programming

Transcript

  1. TypeSpec って何? Microsoft 製の OSS — API ・データ契約の定義言語 書き味は TypeScript

    ライク (model / op / @decorator) 1 つの .tsp → OpenAPI / JSON Schema / Protobuf に変換 @service(#{ title: "Pet Store" }) namespace PetStore; model Pet { id: string; name: string; } @route("/pets") op listPets(): Pet[]; TypeSpec とは
  2. バリデーションも、型に焼き込む TypeSpec は単なる型定義だけじゃない。 正規表現 / 長さ制限 / 値域 といったバリデーションを 型と一体で書ける。

    @pattern("^[a-z0-9]{8,}$") scalar UserId extends string; model User { id: UserId; @minLength(8) password: string; @minValue(0) @maxValue(120) age?: int32; } この制約は OpenAPI 経由で Zod にも変換でき、実行時バリデーション としても効く バリデーション
  3. 設計ルールも、tsp compile で縛れる TypeSpec の Custom Linter で、組織固有の設計ルールを tsp compile

    時に強制できる。 例: 「string scalar には @pattern 必須」をルール化、違反は warning 。 createRule({ name: "require-pattern", severity: "warning", create: (ctx) => ({ scalar: (s) => isStringScalar(s) && !hasPattern(s) && ctx.reportDiagnostic({ target: s }), }), }); bad.tsp:4:8 - warning scalar 'UserId' は string を継承していますが、 @pattern が指定されていません。 > scalar UserId extends string; ^^^^^^ Found 1 warning. 契約の 書き方 だけでなく 設計ルール までガードレールに TypeSpec の拡張性
  4. 私たちの使い方 ① プロダクト内 .tsp → tsp compile → OpenAPI YAML

    ├─ openapi-typescript ─→ TS 型 └─ orval ──────────────→ Zod / API クライアント / MSW モック フロント (React): フォーム validation / API 呼び出し / テストモック バックエンド (Hono): リクエスト validation / レスポンス型 「パスワード 8 文字以上」を TypeSpec で 1 行書けば、FE ・BE ・テスト全部に伝播 使い方 ①
  5. サービス間では、契約が型として存在しない # dev-cross-team サービス A エンジニア 3 分前 @team Webhook

    のペイロード、これって 何型ですか? サービス B エンジニア 2 分前 Notion 見たけど 3 ヶ月前から更新止まってる… サービス C エンジニア 1 分前 サンプル JSON から 型を推測しました… 人手で守るしかなく、コンパイラの出番がない サービス間の課題
  6. 同期 API も 非同期 Webhook も 同じ言語で書ける 同期 API は

    OpenAPI 、非同期メッセージは AsyncAPI / 独自スキーマ、と 別の言語で書くのが普通。 TypeSpec なら 1 つの .tsp に 両方の契約を同居 できる。 // 同期 API(@route + op で表現) @route("/applications") op listApplications(): Application[]; // 非同期 Webhook イベント(model で純粋なデータ契約として表現) model EntityApprovedEvent { type: "entity.approved"; data: ApprovalData; } どちらも 同じ TypeSpec ソースから生成される契約として扱える 独自ポイント
  7. ドキュメントも、契約と一緒に書ける OpenAPI YAML 管理だと 型と説明文が別場所 で並走しがち。 TypeSpec なら /** ...

    */ で 型と同じ場所に 書け、OpenAPI に自動反映される。 /** 法人ユーザー(管理者を含む) */ model User { /** 一意 ID (8 文字以上) */ id: UserId; /** 連絡先メールアドレス */ email: string; } User: type: object description: 法人ユーザー(管理者を含む) properties: id: description: 一意 ID (8 文字以上) email: description: 連絡先メールアドレス 型とドキュメントが 同じソース で管理されるから、説明が古くなりにくい 管理の co-location
  8. 私たちの使い方 ② サービス間 上流チームが TypeSpec で書いた契約は npm パッケージとして公開される。 使う側のプロダクトは npm

    install + import するだけで取り込める。 上流チーム TypeSpec で契約定義 → ビルド → npm publish → npm レジストリ 📦 @org/contracts 型 / Zod / API クライアント → 使う側のプロダクト npm install + import 別チーム / 別サービス "dependencies": { "@org/contracts": "^1.0.0" } import { EntityApprovedEvent } from '@org/contracts' EntityApprovedEvent.parse(payload) サービス境界を跨ぐ契約を、ドキュメントから「型」へ 使い方 ②
  9. サービス跨ぐ契約変更を、マージ前に検知する 上流が破壊的変更を含む新 version を publish すると、Renovate / Dependabot 等の依存更新 bot

    が自動で PR を起票する。 CI checks ✓ lint ✗ typecheck error TS2339: 'oldField' does not exist on 'NewEvent' ❌ Merge blocked マージ前に、契約変更の影響範囲が型エラーとして列挙される 効果
  10. Go 中心のチームでも、TypeSpec で契約を書ける とあるチームの実装は Go ベース。 それでも契約定義は TypeSpec を採用 している。

    書き味は TypeScript ライク Go メイン実装でも、契約 DSL の学習コスト は低い 出力は実装言語に依存しない OpenAPI に変換すれば Go の codegen にも 繋がる 複数チームの共通言語 上流 (Go) と下流 (TS) が同じ TypeSpec を見 て契約を共有 契約を書く言語と、サーバーの実装言語は独立して選べる 拡がり
  11. 今日の話を 3 行でまとめると 1 API も Webhook も、TypeSpec 1 ファイル

    で書ける 同期 API ・非同期イベント・バリデーション・設計ルールまで、同じ DSL に集約 2 書いた契約は npm で配れて、破壊的変更は CI で落ちる サービス境界の契約が package.json の依存として存在し、影響範囲はコンパイルタイムに列挙される 3 TypeScript 以外 の実装言語でも、契約は TypeSpec で書ける 契約 DSL と、サーバーの実装言語は独立して選べる — 異なる技術スタックの共通言語になる 契約は ドキュメント ではなく コード、そして ガードレール へ 総括