
こんにちは。UPSIDER でエンジニアリングマネージャーをしている mitsui です。
2026 年 5 月 22 日(金)に開催された TSKaigi 2026 にて、「TypeSpec で築く複数プロダクトの型安全」というテーマで登壇しました。
TSKaigi は TypeScript をテーマにしたカンファレンスです。言語そのものの活用ノウハウから、それを支えるエコシステムまで、TypeScript にまつわる実践知が集まる場です。
この記事では、当日の発表内容に加えて、時間の都合で話しきれなかった補足も交えながら、UPSIDER での TypeSpec 活用について紹介します。
発表の背景
今回の発表テーマは、「TypeSpec を起点に、複数プロダクト・複数チームのあいだで型安全な契約をどう守るか」です。
プロダクト内であれば、コード生成によってフロントエンド・バックエンド間の型を揃え、tsc などでズレをビルド時に検出できます。
一方、私たちの当時の運用では、サービス間の契約を TypeScript の型として継続的に取り込む仕組みが十分ではなく、tsc の守備範囲の外になっていました。
実際に、Webhook ペイロードの型を Slack で確認したり、サンプル JSON から型を推測してコードを書いたりする場面がありました。
そこで私たちは、TypeSpec を使ってサービス間の契約を「ドキュメント」ではなく「コード」として扱う運用に切り替えました。
同じように、複数プロダクト・複数サービスをまたぐ型安全に悩んでいる方に、少しでも参考になればと思っています。
TypeSpec とは
TypeSpec は、Microsoft が OSS として公開している API・データ契約の定義言語です。
書き味は TypeScript に近く、model / op / @decorator といった構文で API やデータ構造を記述できます。1 つの .tsp ファイルから OpenAPI、JSON Schema、Protocol Buffers などの形式に変換できます。
@service(#{ title: "Pet Store" })
namespace PetStore;
model Pet {
id: string;
name: string;
}
@route("/pets")
op listPets(): Pet[];
TypeSpec の強みの 1 つは、バリデーションルールも契約定義に含められることです。
正規表現、長さ制限などのルールを型定義と同じ場所に書けます。tsp compile 経由で OpenAPI に出力し、さらに orval などで Zod スキーマに変換すれば、コンパイル時の型チェックだけでなく、実行時バリデーションにも活用できます。
@pattern("^[a-z0-9]{8,}$")
scalar UserId extends string;
model User {
id: UserId;
@minLength(8)
password: string;
@minValue(0)
@maxValue(120)
age?: int32;
}
拡張:Custom Linter で設計ルールもガードレールにする
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 }), }), });
LSP 統合によってエディタ上でも warning が表示されるため、CI を待たずに気づける点も便利です。
私たちは現状、CI 上の外部スクリプトで同等のチェックを回していますが、将来的には Custom Linter に寄せることで、tsp compile の流れにより自然に統合できると考えています。
契約の書き方だけでなく、設計ルールまでガードレールにできる点は、TypeSpec の大きな魅力の 1 つです。
私たちの使い方 1:プロダクト内での型共有
私たちのチームでは、TypeScript フルスタックのプロダクトを複数運用しています。その中で、TypeSpec を起点にした次のようなパイプラインを使っています。
.tsp → tsp compile → OpenAPI YAML
├─ openapi-typescript ─→ TypeScript 型
└─ orval ──────────────→ Zod / API クライアント / MSW モック
生成物は、フロントエンド・バックエンド・テストの各領域で利用しています。
- フロントエンド(React)
- フォームバリデーション
- API 呼び出し
- テストモック
- バックエンド(Hono)
- リクエストバリデーション
- レスポンス型
たとえば「パスワードは 8 文字以上」というルールを TypeSpec に 1 行書けば、そのルールをフロントエンド・バックエンド・テストに伝達できます。
ここまでは、TypeScript フルスタックにおける型共有の話として、それほど珍しくないかもしれません。
私たちにとって TypeSpec が特に効いてきたのは、この先のサービス間の領域でした。
私たちの使い方 2:サービス間の契約管理
「契約が型として存在しない」問題
プロダクト内の型共有はある程度解けていても、チームやサービスをまたいだ瞬間に難易度が上がります。
たとえば、次のような状態です。
- Slack で「Webhook のペイロードって何型ですか?」と聞き合う
- Notion を見ても、3 ヶ月前から更新が止まっている
- サンプル JSON から型を推測してコードを書く
この状態では、人手で契約を守るしかありません。コンパイラによるフィードバックも効きづらく、契約の変更に気づくタイミングが遅れがちになります。
同期 API も Webhook イベントも、同じ TypeSpec に書く
このような課題に対して、私たちは契約をできるだけコードベースに落とし込むことにしました。
TypeSpec では、同期 API だけでなく、Webhook イベントのようなデータ契約も同じ .tsp ファイルに記述できます。
// 同期 API
@route("/applications")
op listApplications(): Application[];
// Webhook イベント
model EntityApprovedEvent {
type: "entity.approved";
data: ApprovalData;
}
同期 API と Webhook イベントでは粒度や使われ方は異なります。
それでも、どちらもサービス間で共有される「契約」です。TypeSpec 上に同居させることで、API も Webhook も、同じ契約定義の流れで管理できます。
これにより、サービス間で「API は OpenAPI、Webhook は別のドキュメントやサンプル JSON」といった形で分散していた契約を、より統合的に扱えるようになりました。
ドキュメントも契約と一緒に管理する
OpenAPI YAML を直接管理していると、型定義と説明文が別々の場所で更新されることがあります。その結果、型は新しいのに説明文が古い、あるいはドキュメントだけ更新されて実装側に反映されていない、といったズレが起きやすくなります。
TypeSpec では、doc comment を書くことで、生成される OpenAPI YAML の description フィールドに説明を反映できます。
/** 法人ユーザー(管理者を含む) */
model User {
/** 一意 ID(8 文字以上) */
id: UserId;
/** 連絡先メールアドレス */
email: string;
}
型と説明を同じソースで管理できるため、片方だけが古くなる状態を避けやすくなります。
npm パッケージとして契約を配る
私たちの運用では、上流のチームが TypeSpec で契約を書き、それをビルドして npm パッケージとして公開します。
{ "dependencies": { "@org/contracts": "^1.0.0" } }
import { EntityApprovedEvent } from "@org/contracts"; EntityApprovedEvent.parse(payload);
これにより、契約は Notion だけで参照するものではなく、package.json の依存としてコードベースに存在する状態になります。
契約変更の影響範囲を PR 上で検知する
この仕組みの大きな効果は、契約変更の影響に早く気づけることです。
たとえば、上流のチームが型を変更し、新しい major version を publish したとします。
すると、Renovate や Dependabot などの依存更新 bot が自動で PR を作成します。その PR の CI で tsc が走り、契約と一致しない箇所が型エラーとして検知されます。
つまり、誰かが手元で気づくのを待つのではなく、PR チェックが赤くなることで、契約変更の影響をレビュー前に把握できます。
もちろん、これだけで契約違反を完全に防げるわけではありません。実行時の互換性、リリース順序、後方互換性の設計など、ほかにも考えるべきことはあります。
それでも、サービス境界の変更を型エラーとして早い段階で検知できることは、複数プロダクト・複数チームで開発するうえで大きな安心材料になります。
Go 中心のチームでも TypeSpec で契約を書ける
TypeSpec は、TypeScript チーム専用のツールではありません。
私たちの上流には Go を中心に開発しているチームもありますが、そのチームでも契約定義には TypeSpec を採用しています。
理由は大きく 3 つあります。
- TypeScript ライクな書き味で、契約 DSL としての学習コストが比較的低い
- OpenAPI などに変換できるため、Go の codegen 系ワークフローともつなげやすい
- 上流の Go チームと下流の TypeScript チームが、同じ TypeSpec ファイルを共通言語として扱える
契約を書く言語と、サーバーの実装言語は独立して選べます。
これは、複数の技術スタックを抱える組織にとって大きな利点です。
発表のポイント
今回の発表では、主に次の 3 点をお伝えしました。
1. API も Webhook も TypeSpec に書ける
同期 API、Webhook イベント、バリデーションルール、設計ルールを、同じ TypeSpec の流れで扱えます。
2. 書いた契約は npm で配れて、破壊的変更は CI で検知できる
サービス境界の契約を package.json の依存として取り込み、契約変更の影響をコンパイルタイムに検知できます。
3. TypeScript 以外の実装言語でも、契約は TypeSpec で書ける
契約 DSL とサーバーの実装言語は独立して選べます。異なる技術スタックを持つチーム同士でも、TypeSpec を共通言語として扱えます。
一言でまとめるなら、今回伝えたかったのは「契約はドキュメントではなくコード、そしてガードレールへ」ということです。
スキーマ駆動の開発を、もう一段厳格に運用へ落とし込む。そのための選択肢として、TypeSpec はかなり有力だと感じています。
発表資料
当日の発表資料はこちらです。
おわりに
TSKaigi 2026 では、自分の発表だけでなく、他のセッションや懇親会を通じても多くの刺激を受けました。
TypeScript エコシステムの広がりや、各社が型安全の実現にどう向き合っているのかを直接聞ける場は、とても貴重でした。
今回紹介した「サービス境界を越える型契約」の運用は、私たちのチームでもまだ進化の途中です。
同じように、複数プロダクト・複数チームをまたぐ型契約に悩んでいる方がいれば、ぜひお話ししましょう。
We’re hiring!
UPSIDER では、現在エンジニアを積極採用しています。
複数プロダクト・複数サービスをまたぐ型安全な開発体験を、一緒につくっていきたい方は、ぜひお気軽にご応募ください。
UPSIDER Engineering Deckはこちら📣