型システムの深掘り・実践設計・パフォーマンス最適化まで

コードで学ぶ。型で設計する。パフォーマンスで仕上げる。


この記事で得られること

  • TypeScriptの型システムを「使う」から「設計に活かす」レベルへ引き上げる
  • 実務で使える実践的なパターンと設計手法
  • ビルド・パフォーマンスの最適化テクニック

対象読者:TypeScriptを書いたことはあるが、もっと深く使いこなしたい方


Part 1: 型システムの深掘り

1-1. 型推論の仕組みを理解する

TypeScriptの型推論は見た目以上に賢いです。まず「推論に任せる場所」と「明示すべき場所」を区別しましょう。

// ✅ 推論に任せてよい場所
const user = { id: "1", name: "Masaki" }; // { id: string; name: string }
const nums = [1, 2, 3];                   // number[]
const double = (x: number) => x * 2;      // (x: number) => number

// ✅ 明示すべき場所(推論が意図と違う、または関数の公開API)
function fetchUser(id: string): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

// ❌ 推論できるのに書いている(ノイズになる)
const name: string = "Masaki";
const count: number = 0;

widening(型の拡大)に注意する

// let は型が widening される
let status = "loading";    // string("loading"ではない!)
const status2 = "loading"; // "loading"(リテラル型)

// 意図的にリテラル型にしたい場合
let status3 = "loading" as const; // "loading"

satisfies 演算子で型チェックと推論を両立(TS 4.9+)

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
} satisfies Record<string, string | number[]>;

// satisfies なし → string | number[]
// satisfies あり → 元の型を保持しつつ制約チェック
palette.red;   // number[](string | number[] ではない)
palette.green; // string

1-2. Union型とNarrowing の完全攻略

4種類のNarrowing手法

type Value = string | number | null | { label: string };

function process(val: Value) {
  // 1. typeof
  if (typeof val === "string") {
    console.log(val.toUpperCase()); // string
  }

  // 2. instanceof
  if (val instanceof Date) {
    console.log(val.toISOString()); // Date
  }

  // 3. in演算子
  if (val !== null && "label" in val) {
    console.log(val.label); // { label: string }
  }

  // 4. truthiness
  if (val) {
    // null / undefined / 0 / "" が除外される
  }
}

User-Defined Type Guard でカスタムNarrowingを作る

interface Cat { meow(): void }
interface Dog { bark(): void }
type Animal = Cat | Dog;

// 型ガード関数
function isCat(animal: Animal): animal is Cat {
  return "meow" in animal;
}

function makeSound(animal: Animal) {
  if (isCat(animal)) {
    animal.meow(); // Cat と確定
  } else {
    animal.bark(); // Dog と確定
  }
}

Exhaustiveness Check で全ケース漏れを防ぐ

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default:
      // ここに到達したらコンパイルエラー(全ケース網羅を強制)
      const _exhaustive: never = shape;
      throw new Error(`Unknown shape: ${_exhaustive}`);
  }
}
// 新しい Shape を追加したら、area() でコンパイルエラーが出るため漏れに気づける

1-3. Generics の実践的活用

制約付きGenerics

// K は T のキーに限定する
function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
  return arr.map(item => item[key]);
}

const users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
const names = pluck(users, "name"); // string[]
const ages  = pluck(users, "age");  // number[]
pluck(users, "email");              // ❌ コンパイルエラー

Default Type Parameters(TS 5.0+)

interface PaginatedResponse<T, Meta = { total: number; page: number }> {
  data: T[];
  meta: Meta;
}

type UserList = PaginatedResponse<User>; // Meta はデフォルトの型を使用
type CustomList = PaginatedResponse<Post, { cursor: string }>;

1-4. 高度な型操作

Conditional Types で型を計算する

// null / undefined を除去する
type NonNullable<T> = T extends null | undefined ? never : T;

// Promise を unwrap する
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

// 関数の引数の型を取り出す
type Parameters<T extends (...args: any[]) => any> =
  T extends (...args: infer P) => any ? P : never;

// 実用例:APIレスポンスから型を生成
type ApiResponse<T> = T extends { data: infer D } ? D : never;

Mapped Types で型を変換する

// 全プロパティをDeepReadonlyにする
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// キーをremapする(TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User { name: string; age: number }
type UserGetters = Getters<User>;
// => { getName: () => string; getAge: () => number }

Template Literal Types でAPI型安全性を高める

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type Resource = "users" | "posts" | "comments";
type ApiRoute = `/${ApiVersion}/${Resource}`;

// イベント名の型安全な定義
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"

Part 2: 実践的なパターン・設計

2-1. Discriminated Union で状態設計を極める

フロントエンドでの非同期状態管理

// ❌ よくある壊れやすい実装
type State = {
  isLoading: boolean;
  data: User[] | null;
  error: Error | null;
};
// isLoading: true && data: [...] という矛盾状態が作れる

// ✅ 状態を排他的に定義する
type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

// React での使用例
function UserList() {
  const [state, setState] = useState<AsyncState<User[]>>({ status: "idle" });

  const fetchUsers = async () => {
    setState({ status: "loading" });
    try {
      const data = await api.getUsers();
      setState({ status: "success", data });
    } catch (error) {
      setState({ status: "error", error: error as Error });
    }
  };

  switch (state.status) {
    case "idle":    return <button onClick={fetchUsers}>Load</button>;
    case "loading": return <Spinner />;
    case "success": return <List items={state.data} />; // data は確実に存在
    case "error":   return <ErrorView error={state.error} />;
  }
}

2-2. Result型パターンで例外を型安全に扱う

// Result型の定義
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

// ヘルパー関数
const ok = <T>(value: T): Result<T, never> => ({ ok: true, value });
const err = <E>(error: E): Result<never, E> => ({ ok: false, error });

// 使用例
async function parseJSON<T>(text: string): Promise<Result<T, SyntaxError>> {
  try {
    return ok(JSON.parse(text) as T);
  } catch (e) {
    return err(e as SyntaxError);
  }
}

// 呼び出し側で例外処理が強制される
const result = await parseJSON<User>(responseText);
if (result.ok) {
  console.log(result.value.name); // User と確定
} else {
  console.error(result.error.message); // SyntaxError と確定
}

2-3. Builder パターンで型安全なAPI設計

class QueryBuilder<T extends Record<string, unknown>> {
  private filters: Partial<T> = {};
  private _limit = 10;
  private _offset = 0;

  where<K extends keyof T>(key: K, value: T[K]): this {
    this.filters[key] = value;
    return this;
  }

  limit(n: number): this {
    this._limit = n;
    return this;
  }

  offset(n: number): this {
    this._offset = n;
    return this;
  }

  async execute(): Promise<T[]> {
    // DB クエリ実行
    return fetch(`/api/query?${new URLSearchParams({
      filters: JSON.stringify(this.filters),
      limit: String(this._limit),
      offset: String(this._offset),
    })}`).then(r => r.json());
  }
}

// 使用例
const users = await new QueryBuilder<User>()
  .where("status", "active") // status は User のキー以外不可
  .limit(20)
  .offset(0)
  .execute();

2-4. zodで実行時検証と型を一元管理する

import { z } from "zod";

// スキーマが型定義を兼ねる(二重管理を排除)
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(50),
  email: z.string().email(),
  role: z.enum(["admin", "member", "guest"]),
  createdAt: z.coerce.date(), // 文字列を自動でDateに変換
});

const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserSchema = UserSchema.partial().required({ id: true });

type User = z.infer<typeof UserSchema>;
type CreateUserInput = z.infer<typeof CreateUserSchema>;

// APIハンドラーでの使用
async function createUser(req: Request) {
  const body = await req.json();
  const input = CreateUserSchema.safeParse(body);

  if (!input.success) {
    return Response.json(
      { errors: input.error.flatten() },
      { status: 400 }
    );
  }

  // input.data は CreateUserInput 型が保証されている
  const user = await db.users.create(input.data);
  return Response.json(user);
}

2-5. Branded Types で同じ型の混同を防ぐ

// 同じ string でも意味が違う場合に混同を防ぐ
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { [__brand]: B };

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

// コンストラクタ関数
function UserId(id: string): UserId {
  if (!id.match(/^user_/)) throw new Error("Invalid UserId");
  return id as UserId;
}

function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }

const userId = UserId("user_123");
const postId = "post_456" as PostId;

getUser(userId); // ✅
getUser(postId); // ❌ コンパイルエラー!PostId を UserId に渡せない

Part 3: パフォーマンス・ビルド最適化

3-1. TypeScript コンパイル速度の最適化

tsconfig.json の最適設定

{
  "compilerOptions": {
    // 型チェックのみ(トランスパイルはesbuild/SWCに任せる)
    "noEmit": true,

    // インクリメンタルコンパイルで再ビルドを高速化
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo",

    // 必要なlibだけ指定(不要な型定義の読み込みを避ける)
    "lib": ["ES2022", "DOM"],
    "target": "ES2022",

    // プロジェクト参照で大規模プロジェクトを分割コンパイル
    "composite": true,

    // 厳格モードは必ず有効に
    "strict": true,

    // 型のみのimportを明示(バンドラー最適化に有効)
    "verbatimModuleSyntax": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Project References で大規模プロジェクトを高速化

monorepo/
├── tsconfig.json      # ルート(参照のみ)
├── packages/
│   ├── core/          tsconfig.json(composite: true)
│   ├── ui/            tsconfig.json(composite: true, references: [core])
│   └── app/           tsconfig.json(composite: true, references: [ui, core])
// packages/app/tsconfig.json
{
  "compilerOptions": { "composite": true },
  "references": [
    { "path": "../core" },
    { "path": "../ui" }
  ]
}

これにより変更があったパッケージのみ再コンパイルされます。


3-2. 型チェックと型定義のパフォーマンス

重い型は避ける

// ❌ 深いネストの条件型はコンパイルを遅くする
type DeepUnwrap<T> = T extends Promise<infer U>
  ? DeepUnwrap<U>
  : T extends Array<infer U>
  ? DeepUnwrap<U>
  : T;

// ✅ interface のマージは type より高速
interface Base { id: string }
interface Extended extends Base { name: string } // ✅ 速い

type ExtendedType = Base & { name: string }; // △ 毎回計算される

type-fest で型ユーティリティを再利用する

自前で複雑な型を実装する前に type-fest を確認しましょう。

import type { Except, Merge, RequireAtLeastOne, ReadonlyDeep } from "type-fest";

type UserWithoutPassword = Except<User, "password">;
type MergedConfig = Merge<DefaultConfig, UserConfig>;

// 少なくとも1つのプロパティが必要な型
type SearchParams = RequireAtLeastOne<{
  name?: string;
  email?: string;
  id?: string;
}>;

3-3. バンドルサイズの最適化

type-only import で不要なコードを除去

// ❌ 型なのにランタイムコードに残る可能性がある
import { User } from "./types";

// ✅ type-only import は必ずバンドルから除去される
import type { User } from "./types";

// ✅ verbatimModuleSyntax を tsconfig で有効にすると強制される

const enum の罠

// ❌ const enum はトランスパイラ(esbuild等)と相性が悪い
const enum Direction {
  Up,
  Down,
}

// ✅ as const を使う(ランタイムオブジェクトが残るが安全)
const Direction = {
  Up: 0,
  Down: 1,
} as const;
type Direction = typeof Direction[keyof typeof Direction];

3-4. 実行時パフォーマンスと型設計の関係

オブジェクトの形状を統一してV8最適化を活かす

// ❌ 動的にプロパティを追加するとHidden Classが壊れる
function createUser(name: string) {
  const user: any = {};
  user.name = name;
  user.id = Math.random(); // 後から追加
  return user;
}

// ✅ 最初から完全な形状で作る(型でも強制される)
function createUser(name: string): User {
  return {
    id: crypto.randomUUID(),
    name,
    createdAt: new Date(),
  };
}

Readonly を使って意図しないミューテーションを防ぐ

// 配列のReadonly
function sumArray(arr: readonly number[]): number {
  // arr.push(1); // ❌ コンパイルエラー
  return arr.reduce((a, b) => a + b, 0);
}

// 深いReadonly
type Config = Readonly<{
  db: Readonly<{
    host: string;
    port: number;
  }>;
}>;

まとめ:最強TSエンジニアのチェックリスト

型システム編

  • [ ] any を使わず unknown か適切な型で代替できる
  • [ ] Discriminated Union で状態を排他的に設計できる
  • [ ] satisfies 演算子を使いこなせる
  • [ ] Conditional Types・Mapped Types・Template Literal Types を書ける
  • [ ] Exhaustiveness Check を実装できる
  • [ ] Branded Types で意味的な型安全性を実現できる

設計パターン編

  • [ ] Result型パターンで例外を型安全に扱える
  • [ ] zod でスキーマと型を一元管理できる
  • [ ] Builder パターンで型安全なAPIを設計できる
  • [ ] 状態管理を Discriminated Union で設計できる

パフォーマンス編

  • [ ] incremental ビルドを設定している
  • [ ] type-only import を使い分けられる
  • [ ] const enum の罠を知っている
  • [ ] verbatimModuleSyntax を有効にしている
  • [ ] 大規模プロジェクトで Project References を使える

参考リソース

リソース内容
TypeScript公式ドキュメント最も正確な一次情報
TypeScript Playground型の動作を即確認
Total TypeScriptMatt Pocock による実践的なTS学習
type-fest実用的な型ユーティリティ集
zodスキーマ駆動の型定義
TypeScript Performance Wiki公式パフォーマンスガイド

最後まで読んでいただきありがとうございます。 質問やフィードバックはコメントやXにてお気軽にどうぞ!