# 本番TypeScriptの型安全規律：any禁止・Zodで境界を守り・NeverErrorで網羅性を強制する

> 型安全を「方針」で終わらせない実務ガイド。strict系tsconfig、any/as/enum禁止、Zodで境界をparseするSSoT設計、NeverErrorによる網羅性の強制、satisfies、branded型、CIの型カバレッジまで本番の動くコードで解説します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: TypeScript, アーキテクチャ設計, 型安全性, B2B SaaS, フロントエンド
- URL: https://tomodahinata.com/blog/typescript-type-safety-discipline-zod-nevererror-no-any

## 要点

- TypeScript を使うことと型安全であることは別物で、型安全は層をなす6つの規律の積み重ねである
- tsconfig は strict に加え noUncheckedIndexedAccess・exactOptionalPropertyTypes まで入れて最大出力にする
- 型の3大抜け穴 any / as / enum を断ち、any は unknown、as は parse、enum は as const ユニオンへ置き換える
- 境界では Parse, don't validate で Zod を置き、z.infer で型を導出（SSoT）、safeParse で失敗を値として扱う
- 判別可能ユニオン＋NeverError で網羅性をコンパイルエラー化し、no-explicit-any・型カバレッジを CI で強制する

---

「うちはTypeScriptだから型安全です」——この一文ほど、当てにならない言葉はありません。`any` が一箇所でも混ざれば、その先のコードは全部「願望ベースのコーディング」になります。`as` でエラーを黙らせれば、コンパイラはもう守ってくれません。**TypeScriptを使うことと、型安全であることは、まったく別物**です。

この記事は「型安全を、方針ではなく**規律（discipline）**として本番に根付かせる」ための実務ガイドです。私は複数名チームの [サブスク学習プラットフォーム](/case-studies/subscription-learning-platform)（Next.js 16 + Turborepo モノレポ）と、Supabase + Expo のモノレポの双方で、**`any`/`as`/`enum` の原則禁止・NeverError による網羅検査・CIでの型カバレッジ計測**という同じ規律を敷いています。その実装レベルの判断軸を、TypeScript 公式・Zod 公式に忠実に、しかし公式より**判断軸を厚く**して書きます。

> 本記事の基準：**TypeScript 5.x 系**（`satisfies`・`const` 型パラメータは 5.0、`using`/`Disposable` は 5.2 で導入）、**Zod 4（stable）**。API は [typescriptlang.org](https://www.typescriptlang.org/tsconfig) と [zod.dev](https://zod.dev) で確認した現行仕様に基づきます。

なお、**異なる言語・別リポジトリにまたがる「エンドツーエンド型安全」**（OpenAPI 契約優先）は別記事 [Next.js 16 × Go × OpenAPI](/blog/nextjs-go-openapi-end-to-end-type-safety) に、**決済ドメインに型安全を効かせた具体**は [サブスク決済の冪等性と型安全](/blog/subscription-platform-billing-idempotency-type-safety) に書きました。本記事はそれらの土台となる「**1リポジトリ内の TypeScript そのものの規律**」に集中します。

## 0. 全体像：型安全を支える「6つの規律」

型安全は単一の機能ではなく、**層をなす規律の積み重ね**です。下の層が崩れると上は意味をなしません。

| 層 | 規律 | 目的 | 本記事の章 |
| --- | --- | --- | --- |
| **基盤** | 厳格な `tsconfig` | コンパイラを最大出力で働かせる | §1 |
| **言語** | `any`/`as`/`enum` を断つ | 型の抜け穴を塞ぐ | §2 |
| **境界** | Zod で parse する | 外部入力を信頼しない | §3 |
| **モデル** | 判別可能なユニオン | 不正な状態を表現不能にする | §4・§6 |
| **網羅** | NeverError | 分岐漏れをコンパイルエラーにする | §5 |
| **同一性** | branded 型 | IDや金額の取り違えを防ぐ | §7 |

そしてこれらを **CI で強制**して初めて、規律はチーム規模でも生き残ります（§8）。順に見ていきます。

---

## 1. 基盤：tsconfig はコンパイラの「出力設定」である

まず最初にやるべきは、コンパイラを**フルパワーで**動かすことです。`strict: true` は出発点であって終点ではありません。[tsconfig リファレンス](https://www.typescriptlang.org/tsconfig)が定義する追加フラグまで入れて初めて、現場で効く防御になります。

```jsonc
// tsconfig.json — 本番推奨の最小強化セット
{
  "compilerOptions": {
    "strict": true,                          // 下記strict系を一括有効化
    "noUncheckedIndexedAccess": true,        // 配列/インデックスアクセスに undefined を付与
    "exactOptionalPropertyTypes": true,      // `?:` と `| undefined` を区別する
    "noImplicitOverride": true,              // override キーワードを必須化
    "noImplicitReturns": true,               // 全パスで return を強制
    "noFallthroughCasesInSwitch": true,      // switch のフォールスルーを禁止
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "verbatimModuleSyntax": true,            // 実行時に成立しない import/export を禁止
    "isolatedModules": true                  // 1ファイル単位のトランスパイル安全性
  }
}
```

`strict: true` が有効化するのは `noImplicitAny`・`strictNullChecks`・`strictFunctionTypes`・`strictPropertyInitialization`・`useUnknownInCatchVariables` などです。**`useUnknownInCatchVariables` が含まれている**のが重要で、これにより `catch (e)` の `e` は `any` ではなく `unknown` になります（§3で活きます）。

### 1-1. なぜ `noUncheckedIndexedAccess` を入れるか

これは**入れていないチームが圧倒的に多いが、最も事故を防ぐ**フラグです。

```ts
// noUncheckedIndexedAccess: false（既定）—— 嘘の型
const users: string[] = [];
const first = users[0];        // 型は string（だが実体は undefined！）
first.toUpperCase();           // 実行時に TypeError、コンパイルは通る

// noUncheckedIndexedAccess: true —— 正直な型
const first2 = users[0];       // 型は string | undefined
first2.toUpperCase();          // コンパイルエラー：Object is possibly 'undefined'
first2?.toUpperCase();         // ガードを強制される（正しい）
```

配列・`Record`・オブジェクトのインデックスアクセスは、**実体としては常に `undefined` を返しうる**。それを型に反映させるだけのフラグですが、`process.env.FOO`（実体は `string | undefined`）を `string` として扱う事故を構造的に防ぎます。

### 1-2. `exactOptionalPropertyTypes` が分ける2つの意味

「キーが無い」と「キーはあるが値が `undefined`」は、JavaScript では**別物**です。このフラグはそれを型でも区別します。

```ts
interface Settings {
  theme?: "dark" | "light"; // 「省略可能」であって「undefined を代入してよい」ではない
}

const s: Settings = {};
s.theme = undefined; // フラグ ON ならエラー：undefined は許可されていない
// 「キーを消す」意図なら delete s.theme; が正しい
```

`{ theme: undefined }` を JSON にシリアライズするとキーが消える、といった微妙な挙動差がバグの温床になります。意味を型で固定するのが正解です。

> **判断軸：どこまで厳しくするか。** 私は新規リポジトリでは上記フルセットを最初から入れます。**後から有効化するほど修正コストが指数的に増える**ためです。既存の大規模コードに後付けするなら、`noUncheckedIndexedAccess` だけ別ブランチで段階導入し、ファイル単位で潰すのが現実的です。

---

## 2. 言語：型の3大抜け穴 `any` / `as` / `enum` を断つ

tsconfig で土台を固めたら、次は**コードに開いた穴を塞ぐ**番です。型安全を破壊する3つの抜け穴を、優先度順に断ちます。

### 2-1. `any` 禁止、`unknown` + ナローイングで受ける

`any` は「型チェックを無効化する」という意味です。一度通ると**伝播し**、触れたものすべての安全性を奪います。代わりに `unknown`（何でも入るが、絞らないと何もできない）を使います。

| | `any` | `unknown` | 具体型 |
| --- | --- | --- | --- |
| 代入を受ける | ✅ 何でも | ✅ 何でも | ❌ 型が合うものだけ |
| そのまま使う | ✅（**チェックなし＝危険**） | ❌ ナローイング必須 | ✅ 安全 |
| 型エラーの伝播 | ❌ 周囲に伝染する | ✅ 局所に閉じる | ✅ なし |
| 使いどころ | **原則なし** | 外部入力の受け口 | 通常すべて |

```ts
// ❌ any：その先は無検査。プロパティ名のタイポも素通り
function handle(payload: any) {
  return payload.usrId; // typo でも通る → 実行時に undefined
}

// ✅ unknown + ナローイング：使う前に必ず絞る
function handle(payload: unknown) {
  if (
    typeof payload === "object" &&
    payload !== null &&
    "userId" in payload &&
    typeof payload.userId === "string"
  ) {
    return payload.userId; // ここで初めて string として安全に使える
  }
  throw new Error("invalid payload");
}
```

とはいえ手書きのナローイングは冗長です。**外部入力は Zod で一発で絞る**のが実務解で、§3で扱います。

### 2-2. `as` キャストは「コンパイラへの嘘」

[型アサーション](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) `as T` は、**実行時には何もしません**。型チェックを黙らせるだけです。`fetch` の結果に `as User` を付けても、サーバーが違う形を返せば防げません。

```ts
// ❌ 最悪パターン：検証なしのキャスト。実体が違えば即クラッシュ
const res = await fetch("/api/user");
const user = (await res.json()) as User; // JSON.parse は any 相当。as で嘘の型を被せている

// ✅ parse する（§3）。実体と型が一致することを実行時に保証
const user = UserSchema.parse(await res.json());
```

`as` が許容できるのは、**人間がコンパイラより多くを知っている**ごく狭い場合だけ（例：DOM の `getElementById` の戻りを特定要素型へ）です。それでも `as const`（リテラルを widening させない）とは役割が別物である点に注意してください。

```ts
// as const は「キャスト」ではなく「これ以上広げるな」の指示。これは推奨
const ROLES = ["admin", "member", "guest"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "member" | "guest"
```

### 2-3. `enum` を避け、`as const` ユニオン／オブジェクトを使う

[TypeScript 公式の enum ハンドブック](https://www.typescriptlang.org/docs/handbook/enums.html)自身が、`enum` より**`as const` オブジェクト**を推奨しています。理由は明確です。

- **数値 enum は値が不透明**：ログに `2` とだけ出ても意味が読めない。
- **`const enum` はインライン展開される**：依存のバージョン差で値がズレると、`if` の分岐を取り違えるバグになる。`isolatedModules` とも非互換。
- **JavaScript に存在しない構文**：`enum` は TS 独自で、生成される実行時オブジェクトも直感に反する。

公式が示す代替が、これです。

```ts
// ✅ as const オブジェクト + 派生ユニオン型（公式推奨）
const OrderStatus = {
  Pending: "pending",
  Paid: "paid",
  Shipped: "shipped",
  Cancelled: "cancelled",
} as const;

type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
// "pending" | "paid" | "shipped" | "cancelled"

function advance(status: OrderStatus) { /* ... */ }
advance(OrderStatus.Paid); // 値で参照しても良い
advance("paid");           // リテラルでも通る（enum より柔軟）
```

| | `enum` | `as const` ユニオン／オブジェクト |
| --- | --- | --- |
| 実行時の値 | 不透明（数値）になりがち | **そのまま文字列**で読める |
| JS との整合 | ❌ TS独自構文 | ✅ ただのオブジェクト |
| `isolatedModules` | ⚠️ `const enum` は非互換 | ✅ 問題なし |
| Zod 連携 | 噛み合わせにくい | ✅ `z.enum` と一致（§3） |
| バンドル | 余分なヘルパ生成 | 最小 |

**結論：新規コードで `enum` を書く理由はほぼありません。**

---

## 3. 境界：Parse, don't validate —— Zod を信頼境界に置く

ここが規律の心臓部です。原則は **"Parse, don't validate"**。「データが正しいか**確認する**（validate）」のではなく、「正しい型へ**変換する**（parse）」。parse を通った後の値は、型システムが保証する**安全な領域**に入ります。

外部からシステムに入るものは**すべて信頼できない**——API レスポンス、フォーム入力、環境変数、`localStorage`、`JSON.parse` の戻り。これらの境界に [Zod](https://zod.dev)（現行は **Zod 4 stable**）を置きます。

### 3-1. スキーマが唯一の真実源（SSoT）—— `z.infer` で型を導出する

最大の利点は **DRY**：型を手書きせず、スキーマから `z.infer` で導出します。スキーマを変えれば型も自動で追従し、**乖離が原理的に起きません**。

```ts
import { z } from "zod";

// スキーマ（実行時の検証ロジック）を一度だけ定義
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(["admin", "member", "guest"]), // §2-3 のユニオンと一致
  createdAt: z.coerce.date(),
});

// 型はスキーマから導出する（手書きしない＝SSoT）
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; role: "admin"|"member"|"guest"; createdAt: Date }
```

入力（変換前）と出力（変換後）で型が違う場合は `z.input` / `z.output` で取り分けられます（上の `z.coerce.date()` は入力 `string`、出力 `Date`）。

### 3-2. `safeParse` で失敗を「値」として扱う

`parse` は失敗時に例外を投げます。境界では**失敗が想定内**なので、例外ではなく**結果オブジェクト**で扱う `safeParse` を使い、型付きで分岐します。

```ts
// API ルートでのリクエスト検証（信頼境界）
export async function POST(req: Request) {
  const json: unknown = await req.json(); // JSON.parse 相当は unknown 扱いが正しい
  const result = UserSchema.safeParse(json);

  if (!result.success) {
    // result.error は ZodError。型付きで安全にハンドリングできる
    return Response.json({ errors: result.error.flatten() }, { status: 400 });
  }

  // result.data は User 型として保証されている。ここから先は安全地帯
  const user = result.data;
  return Response.json({ id: user.id });
}
```

**セキュリティ的にも本質的**です。OWASP の入力検証は「境界で全部弾く」が原則。Zod スキーマがそのまま入力検証の仕様書になります。

### 3-3. 環境変数も parse する

`process.env.X` は `string | undefined`。直接使うと「本番で環境変数が一個抜けていて静かに壊れる」典型事故になります。**起動時に一括 parse**して落とします（fail fast）。

```ts
// env.ts —— アプリ起動時に一度だけ検証する
const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
  NODE_ENV: z.enum(["development", "production", "test"]),
});

// 不正なら起動時に例外で即死させる（実行時の謎挙動より100倍デバッグしやすい）
export const env = EnvSchema.parse(process.env);
// 以降 env.DATABASE_URL は string として保証される
```

---

## 4. モデル：判別可能なユニオンで「不正な状態を表現不能」にする

型安全の真価は、検証以上に**モデリング**にあります。標語は **"Make illegal states unrepresentable"**——不正な組み合わせを、そもそも型として作れなくする。

ありがちなのは「全部 optional の神オブジェクト」です。

```ts
// ❌ 不正な状態が表現できてしまう
interface Fetch {
  loading: boolean;
  data?: User;
  error?: string;
}
// loading:true なのに data がある、error と data が同時にある…全部作れてしまう
```

[判別可能なユニオン（discriminated union）](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions)で、**ありえる状態だけ**を列挙します。

```ts
// ✅ 取りうる状態だけが型として存在する
type FetchState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }   // success の時だけ data がある
  | { status: "error"; message: string }; // error の時だけ message がある

function render(state: FetchState) {
  switch (state.status) {
    case "success":
      return state.data.email;   // data は success 分岐でのみアクセス可
    case "error":
      return state.message;      // message も同様
    // idle / loading は data を持たない → 触ろうとすると型エラー
  }
}
```

`status` という共通リテラル（判別子）で、TypeScript が各分岐の型を自動で絞り込みます。**「data があるのに loading」のような矛盾状態は、書こうとしても型が許しません。** これが ETC（Easy To Change）にも効きます——状態が増えても、影響箇所はコンパイラが教えてくれる（次章）。

---

## 5. 網羅：NeverError で「分岐漏れ」をコンパイルエラーにする

判別可能なユニオンの真価は、**網羅性チェック**と組み合わせた時に出ます。`never` 型は「ありえない値」を表し、**全ケースを処理し切ると残りが `never` になる**という性質を利用します（[公式の exhaustiveness パターン](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking)）。

これを再利用可能な**カスタムエラー**にしたのが NeverError です。

```ts
// never-error.ts —— 「ここには到達しないはず」を型と実行時の両方で守る
export class NeverError extends Error {
  constructor(value: never) {
    super(`Unreachable: unexpected value ${JSON.stringify(value)}`);
    this.name = "NeverError";
  }
}
```

使い方は、`switch` の `default` で受けるだけです。

```ts
function label(state: FetchState): string {
  switch (state.status) {
    case "idle":    return "待機中";
    case "loading": return "読み込み中";
    case "success": return "完了";
    case "error":   return "エラー";
    default:
      // 全ケースを処理していれば state は never 型 → コンパイル通過
      throw new NeverError(state);
  }
}
```

ここに `{ status: "cancelled" }` をユニオンへ追加すると、`default` で `state` が `never` に収束しなくなり、**`NeverError(state)` がコンパイルエラー**になります。

```text
Argument of type '{ status: "cancelled" }' is not assignable to parameter of type 'never'.
```

これが効く理由は **ETC（変更容易性）と信頼性の両立**です。状態を1つ足すと、**その状態を扱い忘れている全箇所がビルドで赤くなる**。テストを書く前に、コンパイラが「ここも直せ」と全部指してくれる。`enum` + 文字列比較ではこの保証は得られません（§2-3で `enum` を避ける実利のひとつ）。私のサブスク基盤では、課金状態・コミッション台帳・認証状態など、**ドメインの状態機械すべてにこの NeverError パターンを適用**しています。

> **`default: return assertNever(x)` 関数版との違い**：例外クラスにしておくと、スタックトレースに「どの不正値で落ちたか」が残り、本番での原因切り分けが速い。関数版でも網羅性は守れますが、私は観測性のため例外クラスを採ります。

---

## 6. `satisfies`：型チェックしつつ「絞り込みを失わない」

§2 で「`as` は嘘」と書きました。では「この値が型 `T` を満たすことは確認したいが、**リテラルの精度は落としたくない**」時はどうするか。TypeScript 5.0 の [`satisfies` 演算子](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html)がその答えです。

```ts
type RouteConfig = Record<string, { path: string; auth: boolean }>;

// ❌ 注釈（: RouteConfig）—— 型は RouteConfig に「広がる」
const routesA: RouteConfig = {
  home: { path: "/", auth: false },
  dashboard: { path: "/app", auth: true },
};
routesA.home;     // OK だが…
routesA.unknown;  // ⚠️ Record なのでタイポも string キーとして通ってしまう

// ✅ satisfies —— RouteConfig に適合することを検証しつつ、具体的なキーは保持
const routesB = {
  home: { path: "/", auth: false },
  dashboard: { path: "/app", auth: true },
} satisfies RouteConfig;
routesB.home;     // OK
routesB.unknown;  // ✅ コンパイルエラー：存在しないキー
```

`satisfies` は「**制約は課すが、推論された具体型は捨てない**」。`as`（実行時に無力な嘘）とも、型注釈（widening する）とも違う、第三の道です。設定オブジェクト・マッピング・定数テーブルで多用します。関連して 5.0 の `const` 型パラメータ（`<const T>` で `as const` 相当の推論を関数引数に効かせる）も、同じ「精度を落とさない」系の道具です。

---

## 7. branded 型：IDと金額の「取り違え」を型で止める

`string` の `userId` と `string` の `orderId` は、TypeScript の**構造的型付け**では区別されません。引数の順序を間違えても通ってしまう。**branded（nominal）型**で、構造が同じでも別物として扱わせます。

```ts
// 公称型を作る（実行時コストゼロ。型レベルのタグだけ）
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

declare function cancelOrder(user: UserId, order: OrderId): void;

const u = "u_123" as UserId;
const o = "o_456" as OrderId;

cancelOrder(u, o); // ✅
cancelOrder(o, u); // ❌ コンパイルエラー：引数の取り違えを型が検出
```

Zod なら境界の parse と同時に brand を付けられます（[`.brand()`](https://zod.dev/api)）。**「検証済みの値」であること自体を型に焼き込める**のが強力です。

```ts
const UserId = z.string().uuid().brand<"UserId">();
type UserId = z.infer<typeof UserId>; // string & z.$brand<"UserId">

// parse を通った値だけが UserId 型を名乗れる＝「未検証の文字列」と区別される
const id = UserId.parse("550e8400-e29b-41d4-a716-446655440000");
```

**金額**も同じ発想で守ります。`cents`（整数・最小単位）と `yen`（表示）を別の brand にすれば、**100倍ズレた金額をうっかり渡す**事故が型で止まります。決済ドメインでこれをどう徹底したかは [サブスク決済の冪等性と型安全](/blog/subscription-platform-billing-idempotency-type-safety) に詳述しました。

---

## 8. CI：規律を「人間の善意」に頼らない

ここまでの規律は、**強制しなければ必ず腐ります**。レビューの見落とし一回で `any` が入り、次第に伝播する。チーム規模で生き残らせる唯一の方法は、**機械に守らせる**ことです。

### 8-1. ESLint で抜け穴を構文レベルで禁止

[`typescript-eslint`](https://typescript-eslint.io) の型情報つきルールを `error` に設定します。

```jsonc
// eslint 設定（抜粋）
{
  "rules": {
    "@typescript-eslint/no-explicit-any": "error",        // any を書けなくする
    "@typescript-eslint/no-unsafe-assignment": "error",   // any 由来の代入を禁止
    "@typescript-eslint/no-unsafe-member-access": "error", // any のプロパティ参照を禁止
    "@typescript-eslint/no-unsafe-call": "error",
    "@typescript-eslint/no-unsafe-return": "error",
    "@typescript-eslint/consistent-type-assertions": [     // as キャストを抑制
      "error", { "assertionStyle": "never" }
    ]
  }
}
```

`no-unsafe-*` 系が重要で、**`any` を直接書かなくても、`JSON.parse` や型なしライブラリ経由で `any` が紛れ込んだ瞬間に検出**します。これが「`any` 禁止」を実効化します。

### 8-2. tsc と型カバレッジをゲートにする

CI のジョブとして、型チェックと型カバレッジ計測を**マージの必須条件**にします。

```bash
# 1. 型エラーゼロを保証（emit せず型検査だけ）
tsc --noEmit

# 2. lint（上の no-explicit-any 等）
eslint . --max-warnings 0

# 3. 型カバレッジ：明示的な型がついている割合を計測し、しきい値で落とす
type-coverage --strict --at-least 99 --ignore-files "**/*.test.ts"
```

`type-coverage --strict` は、コード中で `any`（に潰れている箇所）の割合を可視化します。しきい値（例：99%）を下回ったら CI が落ちる。**「いつの間にか型が緩んでいた」を数値で止める**仕組みです。私のモノレポでは、これらを Turborepo のタスクとして全パッケージ横断で回し、PRごとに強制しています。

---

## 9. よくある落とし穴

現場で繰り返し見る、型安全を**自分で無効化してしまう**パターンです。

### 9-1. エラーを黙らせるための `as`

```ts
// ❌ 「とりあえず通す」ための as。バグを未来に先送りしているだけ
const config = loadConfig() as AppConfig;
```

`as` でエラーが消えても、**実体は変わっていません**。コンパイラの警告は「実装が間違っている」サインです。消すべきは警告ではなく原因。境界なら §3 の parse に置き換えます。

### 9-2. 手書きの型ガードが型と乖離する

```ts
// ❌ 型と独立した手書きガード。User にフィールドを足してもガードは更新されない
function isUser(x: unknown): x is User {
  return typeof x === "object" && x !== null && "id" in x; // email を見ていない！
}
```

`is User` という述語は**人間の主張**であって、コンパイラは中身が正しいか検証しません。`User` に `email` を追加してもこのガードは黙って通り、**型と実体が静かに乖離**します。Zod スキーマから `z.infer` すれば、スキーマ＝ガード＝型が常に一致します（§3）。これが「手書きガードを書かない」理由です。

### 9-3. `JSON.parse` の戻りを型として信じる

```ts
// ❌ JSON.parse は any 相当。as で被せた型は何の保証もない
const data = JSON.parse(raw) as ApiResponse;

// ✅ unknown で受けて parse する
const data = ApiResponseSchema.parse(JSON.parse(raw));
```

`JSON.parse` の戻りは実質 `any`。外から来た文字列の中身を、コンパイラは知りようがありません。**必ず parse を挟む**。

### 9-4. `fetch().json()` を型付きと思い込む

§2-2 で触れた通り、`res.json()` の戻りは `Promise<any>`。`as User` は嘘。ネットワーク越しの値こそ最も信頼できない入力です。Zod の `safeParse` で受けるのが唯一の正解です。

### 9-5. `enum` 比較で網羅性を取りこぼす

`enum` + `if/else` の連鎖は、ケースを足しても**コンパイラが漏れを指摘しません**。§4 の判別可能ユニオン + §5 の NeverError に置き換えれば、漏れがビルドで落ちます。

---

## 10. 横断的に効く理由（なぜこの規律が「価値」になるのか）

最後に、この規律が単なる潔癖ではなく**事業価値**である理由を、設計原則で整理します。

- **セキュリティ**：外部入力を境界で全部 parse する（§3）＝ OWASP 入力検証の実装。型が仕様書を兼ねる。
- **保守性 / ETC**：型が変更の影響範囲を局所化する。状態を足すと NeverError が全漏れ箇所を指す（§4・§5）。リファクタが「祈り」でなく「機械的作業」になる。
- **信頼性**：不正な状態を表現不能にする（§4）＝ そもそもバグの種が型として存在できない。
- **DRY**：スキーマが型・検証・ドキュメントの唯一の真実源（§3）。乖離が起きない。
- **テスト容易性**：parse 関数も純粋関数も入出力が型で固定され、テストが書きやすい。型で潰せる領域はテストを書かなくて済む（テストすべきはロジックの分岐だけ）。

型安全は「バグを減らす」だけの話ではありません。**「この設計なら任せられる」とエンタープライズが判断する根拠**そのものです。レビューで毎回「ここ `any` ですよ」と指摘し合う消耗から解放され、人間はドメインの本質的な議論に時間を使えるようになります。

---

## まとめ：型安全は「規律」であって「機能」ではない

要点を最後に6行で。

1. **tsconfig を最大出力に**：`strict` に加え `noUncheckedIndexedAccess`・`exactOptionalPropertyTypes` まで（§1）。
2. **3大抜け穴を断つ**：`any` → `unknown`、`as` → parse、`enum` → `as const` ユニオン（§2）。
3. **境界で parse する**：Zod を信頼境界に置き、`z.infer` で型を導出（SSoT）、`safeParse` で失敗を値として扱う（§3）。
4. **不正な状態を表現不能に**：判別可能ユニオン + **NeverError で網羅性をコンパイルエラー化**（§4・§5）。
5. **精度と公称性**：`satisfies` で widening を防ぎ、branded 型で ID・金額の取り違えを止める（§6・§7）。
6. **CI で強制**：`no-explicit-any`・`no-unsafe-*`・`tsc --noEmit`・型カバレッジをマージ条件に（§8）。

「一人 × 生成AI（Claude Code）で、速く・安く・安全に」作る——その「安全」を担保する背骨が、この型安全規律です。本記事のコードと判断軸は、複数名チームで運用する [サブスク学習プラットフォーム](/case-studies/subscription-learning-platform) で実際に敷いているものです。言語を跨ぐ型安全は [Next.js × Go × OpenAPI](/blog/nextjs-go-openapi-end-to-end-type-safety) を、決済ドメインへの適用は [サブスク決済の冪等性と型安全](/blog/subscription-platform-billing-idempotency-type-safety) を併せてどうぞ。

型安全な設計・既存コードベースの型負債の解消・CIでの品質ゲート構築のご相談は、[お問い合わせ](/contact)からお気軽にどうぞ。
