End-to-End Type Safetyとは
Webシステムは、クライアント(ブラウザ・アプリ)とサーバ、データベース、さらに外部サービスが連携して動いています。
その行き来の中でデータの形(型)がズレてしまうと、開発者の想定と実際が噛み合わず、バグや障害の原因になってしまいます。
End-to-End Type Safety(E2E型安全)は、システムの端から端まで「型」を契約書のように扱い、開発時(コンパイル時)と実行時の両方で破綻しないようにする考え方です。
基本思想
いくつか基本的な考え方を紹介します。
- 単一の真実のソース(Single Source of Truth)
- データ型の定義を一か所にまとめ、そこをサーバやクライアントが参照することで型を統一していきます。
- 境界優先(Boundary First)
- ネットワーク越し、プロセス間、DBとのやり取りなど「境界」では、必ず型の確認とランタイム検証を行います。
- ここで正しいデータの型であることをチェックし、中に不正なデータを入れないようにします。
- コンパイル時の型安全 + 実行時検証の併用
- TypeScriptの型は実行時に消えてしまいます。
- したがって、受信したデータを Zod などで「今まさに動いている状態」で検証することが不可欠です。
- Zodとは、TypeScript向けのスキーマ宣言とデータ検証のためのライブラリです。
- コンパイル時は「間違いに気づくスピードを上げる」、実行時は「侵入させない」と役割を分けて考えます。
- ドメインモデルの明確化
- 「ユーザーID」と「プロジェクトID」を取り違えないように、識別子をブランド化して区別します。
- 互換性とバージョニング
- 破壊的変更(フィールドの削除など)はバージョンを分ける、または Union 型で段階的に移行できるようにします。
- 「any」を避ける
- any は検査をすり抜けるため、最終的に事故の温床になります。
- unknown を受けて、Zod などで絞り込む使い方が安全になります。
- 型の流通経路を意識する
- それぞれ責務を分け、必要に応じて変換レイヤを挟むことで、後からの拡張や差し替えがしやすくなります。
具体的な例
サーバとクライアントが同じ TypeScript で動く環境の場合は、TypeScript の型をそのまま契約として共有できます。
以下はtRPC + Zodを使ったサンプルになります。
サーバ(Node / Express もしくは Fastify)
// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
user: t.procedure
// 入力を Zod で検証(空文字や未定義を拒否)
.input(z.object({ id: z.string().min(1) }))
// 出力も Zod で定義(実行時にも検証可能)
.output(z.object({
id: z.string(),
name: z.string(),
createdAt: z.string().datetime(), // Date は ISO 文字列に変換して返す
}))
.query(async ({ input }) => {
// DB 層は別型(Prisma/Drizzle など)→ DTO に変換して返す
const entity = await getUserById(input.id);
return {
id: entity.id,
name: entity.name,
createdAt: entity.createdAt.toISOString(),
};
}),
});
export type AppRouter = typeof appRouter;
クライアント(React)
// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/trpc';
export const trpc = createTRPCReact();
// 使用例
function UserCard({ id }: { id: string }) {
const { data, isLoading } = trpc.user.useQuery({ id });
if (isLoading) return <span>Loading...</span>;
// createdAt は ISO 文字列なので、表示時に Date オブジェクトへ
return <div>{data!.name}(作成: {new Date(data!.createdAt).toLocaleString()})</div>;
}
ポイントとしては、
- 入力・出力を Zod で定義することで、型安全(TypeScript)とランタイム安全(実際の検証)を両立できる
- サーバとクライアントで同一の型が共有され、自動補完やエラー検知が可能
- 日付や複雑な構造はシリアライズ方針(ISO文字列など)を決めて、境界で変換する というところです。
まとめ
End-to-End Type Safety は、「型=契約」をシステム全体で守ることで、バグの温床になりがちな境界部分を安全にし、開発体験と品質を同時に高める設計思想です。
同言語なら tRPC のような RPC 系で素早く体感でき、異言語・組織間では OpenAPI や GraphQL などのスキーマ駆動で強固な契約を築けます。
入力・出力のスキーマを明確にし、境界で必ず検証する。
これを積み重ねるだけで、システム全体の「型の流れ」が整い、変更に強く、安心してリファクタリングできる基盤が育っていきます。

