← Blog
·10 blog.minutes

モダンTypeScript:2026年のパターンと実践

厳格な設定から型安全なAPIクライアントまで——2026年に本番グレードのコードをおもちゃのプロジェクトから区別するTypeScriptパターン。

TypeScriptはほとんどのエコシステムが追いつける速度以上に進化している。新しいリリースごとに新しい構文、より厳格なチェック、慣用的と感じられるものを書き換えるパターンがもたらされる。単にコンパイルされるTypeScriptコードと、型システムを真に活用するコードの間の差は大きく——そのギャップが、あなたの型がドキュメントなのかノイズなのかを決定する。

この記事では、2026年に本番TypeScriptにとって最も重要なパターンを扱う。これらは学術的な練習問題ではない。大規模コードベースをより安全にし、APIを誤用しにくくし、リファクタリングをより恐ろしくなくする実践である。すべての例は、数百万のリクエストを処理する本番システムで使用されている実際のパターンから来ている。

基盤:2026年のTypeScript設定

TypeScriptの品質に関する最も影響力のある決定はコード内ではなく、tsconfig.jsonにある。ベースラインはstrict: trueを超えて進んだ。2026年には、本番グレードの設定は以前のバージョンではオプトインまたは実験的だったチェックを有効にする必要がある。

// tsconfig.json — the 2026 baseline
{
  "compilerOptions": {
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

各フラグはバグのクラスを排除する。exactOptionalPropertyTypesは、?: string | undefinedに明示的にundefinedが代入される一般的なミスを防ぐ——欠落していることと、存在するがundefinedであることの区別はAPI境界において重要である。noUncheckedIndexedAccessは、動的キーによるオブジェクトアクセスごとにundefinedの処理を強制し、出荷前にランタイムクラッシュをキャッチする。verbatimModuleSyntaxはモジュール解決がランタイムと一致することを保証し、ESMプロジェクトを本番で破壊する静かな不一致を排除する。

この設定を採用したチームは、null参照やundefinedプロパティに関連する本番インシデントが大幅に減少したと報告している。これらの余分なundefinedチェックを事前に処理するオーバーヘッドは、想定していたフィールドがAPIレスポンスに欠けていたためにクラッシュをデバッグするのに比べれば些細なものである。

識別共用体——最も強力なパターン

識別共用体はTypeScriptにおける単一の最も影響力のあるパターンである。状態を明示的にモデル化し、不正な状態を表現不可能にし、コンパイラに網羅的な処理を強制するために必要な情報を提供する。複数の形状を持つデータにこれらを使用していないなら、型システムと戦っているのであり、機能させているのではない。

type ApiState<S, E = Error> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: S }
  | { status: "error"; error: E };

// Exhaustive match — if you add a status, this breaks at compile time
function renderState<S>(state: ApiState<S>): string {
  switch (state.status) {
    case "idle":
      return "Awaiting input";
    case "loading":
      return "Loading...";
    case "success":
      return `Got ${JSON.stringify(state.data)}`;
    case "error":
      return `Failed: ${state.error.message}`;
  }
}

// Usage — impossible to access data on a loading state
const userState: ApiState<User> = { status: "loading" };
// userState.data — does not compile
// userState.status — narrows correctly after any check

魔法は絞り込みにある。state.status === "success"をチェックすると、TypeScriptはどのバリアントにいるかを正確に把握し、そのブランチ上のすべてのフィールドに正しい型を提供する。これにより、ランタイムコードがそうでなければ必要とする防御的チェックのカテゴリ全体が排除される。コンパイラが不正な状態遷移のテストスイートになる。

このパターンを効果的に機能させるには、判別プロパティ(上記の例ではstatus)はリテラル型であり、一般的な文字列であってはならない。各バリアントは一意のリテラル値を持たなければならない。コンパイラはそのリテラルを使用してブランチを区別し、2つのバリアントが同じ判別値を持つ場合に警告する。

  • 判別子として常にリテラル文字列または数値を使用する——一般的な文字列型は決して使わない。
  • 共有プロパティは最小限に保つ。バリアント間で異なるものはすべてバリアントオブジェクト内に属する。
  • never型と組み合わせて網羅性チェックを行う:switchのデフォルトブランチでnever型の変数を宣言し、コンパイル時に未処理のケースをキャッチする。

テンプレートリテラル型とブランド型

テンプレートリテラル型とブランド型は、通常のTypeScript型では対処できない2つの異なる問題を解決する。テンプレートリテラル型は型レベルでの文字列検証を提供する。ブランド型は構造的型付けの世界に名前的型付けをもたらす——同じ形状だが異なる意味論的意味を持つ2つの値を区別できるようにする。

// Template literal type — valid API routes are checked at compile time
type ApiRoute = `/api/${string}`;
type UserRoute = `/api/users/${string}`;

function fetchApi<T>(route: ApiRoute): Promise<T> {
  return fetch(route).then((r) => r.json());
}

// fetchApi("/invalid");       // Error: not assignable
// fetchApi("/api/users/123"); // OK

// Branded type — distinguish IDs that are both strings
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId): Promise<User> {
  return db.users.find(id);
}

const orderId = "ord_123" as OrderId;
// getUser(orderId); // Error: Type 'OrderId' is not assignable to type 'UserId'

テンプレートリテラル型は、文字列が予測可能な形式に従うシステムで輝く。APIルートビルダー、CSSクラスジェネレーター、i18nキーマッチャー、イベント名システムはすべて、文字列値のコンパイル時検証から恩恵を受ける。構文は直感的である——${}プレースホルダーを持つパターンを書き、TypeScriptが実際の値がそのパターンに一致することを検証する。

ブランド型は別の問題を解決する。TypeScriptは構造的型付けであり、同じ形状を持つ2つの型は交換可能である。これは通常は機能だが、両方とも文字列でありながら異なるエンティティを表すIDを持つ場合に危険になる。UserIdとOrderIdは、両方とも文字列であっても交換可能であってはならない。{ __brand: "X" }とのインターセクションは、コンパイル時にのみ存在するファントム型を作成する——ランタイムコストはゼロである。

ブランド型は、追加のツールなしでTypeScriptが名前的型付けに最も近づく方法である。ファントムプロパティとの1つのインターセクションで、間違ったIDが間違った関数に渡されるというクラスのバグを防ぐことができる。

satisfies演算子——犠牲のない推論

satisfies以前は、TypeScript開発者は型に一致しつつリテラル値を保持する必要がある定数を定義する際にジレンマに直面していた。型を注釈して狭い推論を失うか、注釈を省略して検証を失うかのどちらかだった。satisfies演算子はこのトレードオフを完全に排除する。

type ColorPalette = {
  primary: string;
  secondary: string;
  accent: string;
};

// Before satisfies — loses literal types
const paletteOld: ColorPalette = {
  primary: "#0f0f0f",  // type is string, not "#0f0f0f"
  secondary: "#ffffff",
  accent: "#0055ff",
};

// After satisfies — validates shape, keeps literals
const palette = {
  primary: "#0f0f0f",  // type is "#0f0f0f"
  secondary: "#ffffff",
  accent: "#0055ff",
} satisfies ColorPalette;

// palette.primary; // type is "#0f0f0f", not string

// Works with complex types too
type EventMap = {
  click: { x: number; y: number };
  focus: { target: string };
  input: { value: string };
};

const handlers = {
  click: (e: { x: number; y: number }) => console.log(e.x, e.y),
  focus: (e: { target: string }) => console.log(e.target),
} satisfies Partial<Record<keyof EventMap, Function>>;

// handlers.click — retains the parameter type from the implementation
// handlers.input — missing, but satisfies allows Partial
// Add input handler later without breaking anything

satisfies演算子は、形状に対する型安全性が必要だが、値に対しては可能な限り狭い型が必要な設定オブジェクト、イベントハンドラ、マッピング構造で最も価値を発揮する。これは、as constアサーションと型注釈の組み合わせ、冗長な型キャスト、個別の検証関数といった一連の回避策を、単一のキーワードに置き換える。

ジェネリックパターンと型安全なAPIクライアント

TypeScriptのジェネリックは単純なケースでは使いやすい——Array<T>、Promise<T>——しかし、制約、条件型、推論を単一の関数シグネチャで組み合わせると真に強力になる。高度なジェネリックの最も実用的な応用は、ランタイムエラーのカテゴリ全体を排除する型安全なAPIクライアントを構築することである。

// Type-safe API client
import { z } from "zod";

// Infer the output type from a Zod schema
type InferSchema<T extends z.ZodTypeAny> = T["_output"];

// Route definitions with path params and response schema
type RouteDef = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  params?: Record<string, string>;
  body?: unknown;
};

class ApiClient {
  constructor(private base: string) {}

  get<
    TPath extends string,
    TSchema extends z.ZodTypeAny,
  >(
    path: TPath,
    schema: TSchema,
    ...[params]: TPath extends `${string}:${infer _}/${string}`
      ? [Record<string, string>]
      : TPath extends `${string}:${infer _}`
        ? [Record<string, string>]
        : [Record<string, string>?]
  ): Promise<InferSchema<TSchema>> {
    const resolved = params
      ? Object.entries(params).reduce(
          (p, [k, v]) => p.replace(`:${k}`, v),
          path
        )
      : path;
    return fetch(`${this.base}${resolved}`)
      .then((r) => r.json())
      .then((d) => schema.parse(d) as InferSchema<TSchema>);
  }
}

const api = new ApiClient("https://api.example.com");

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

// api.get("/users/:id", userSchema, { id: "123" });
// Result type: { id: string; name: string; email: string }
// If params are required but missing, type error at compile time

ApiClientパターンは複数の高度なテクニックを組み合わせている。inferを使用した条件型は、ルートにパスパラメータが含まれているかどうかを検出し、条件に応じてparams引数を必須にする。レスポンススキーマはZodでランタイムに解析され、コンパイル時に推論されるため、戻り値の型は常に正しい。APIがレスポンス形状を変更した場合、スキーマを更新するとコンパイラが破損するすべてのコンシューマーを見つける。

このパターンは、儀式なしで何百ものエンドポイントにスケールする。各エンドポイントはパス文字列とZodスキーマだけである。ジェネリックインフラが残り——パスパラメータ解決、レスポンス検証、型推論——を処理する。このパターンを使用するチームは、フロントエンドとバックエンド間の統合バグの大部分を排除したと報告している。

  • ZodまたはArkTypeスキーマとジェネリックfetchラッパーを組み合わせて、ネットワーク境界を越えたエンドツーエンドの型安全性を実現する。
  • inferを使用した条件型で、ルート構造に基づいて引数を必須またはオプションにする。
  • APIクライアントからブランド型を返し、呼び出し元が異なるエンティティタイプ間で誤ってIDを交換できないようにする。

エラーハンドリングパターンとモジュール拡張

エラーハンドリングは、ほとんどのTypeScriptコードベースがanyにフォールバックし、問題が存在しないふりをする領域である。Resultパターン——Rustや関数型プログラミングに触発された——はエラーを型システムに取り込み、コンパイラがすべてのエラーパスが処理されることを強制する。モジュール拡張はこのアプローチをサードパーティの型に拡張し、不完全または過度に緩い型定義を出荷するライブラリに型安全性を追加できるようにする。

// Result type — errors are part of the return type, not thrown

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: UserId): Promise<Result<User, ApiError>> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { ok: false, error: await ApiError.fromResponse(res) };
    }
    return { ok: true, value: await res.json() };
  } catch (err) {
    return { ok: false, error: new ApiError("network", String(err)) };
  }
}

// Consumer must handle both branches — compiler enforces it
const result = await fetchUser(userId);
if (result.ok) {
  console.log(result.value.name); // result.value is User
} else {
  console.error(result.error.code); // result.error is ApiError
}

// --- Module augmentation for third-party types ---

// express-session types are incomplete — augment them
declare module "express-session" {
  interface SessionData {
    userId?: string;
    role: "admin" | "user" | "viewer";
    permissions: string[];
  }
}

// Now req.session.role narrows to "admin" | "user" | "viewer"
// Without augmentation, req.session.role would be any

Resultパターンはすべての呼び出し箇所で明示的なエラーハンドリングを強制する。失敗した操作を静かに無視する方法はない——コンパイラはresult.valueにアクセスする前にresult.okをチェックすることを要求する。これにより、無数の本番インシデントの原因である忘れられたtry-catchが排除される。コストは各呼び出し箇所での多少のタイピングだが、メリットはエラーが未処理のままになることがないことである。

モジュール拡張は、サードパーティの型定義が不十分な場合のギャップを埋める。多くの人気ライブラリは過度に寛容な型——anyを返す関数、objectとして型付けされたパラメータ、インターフェースの欠落したプロパティ——を出荷している。asでのキャストや@ts-ignoreの使用ではなく、.d.tsまたは.tsファイルでdeclare module "library-name"を使用して不足している型を追加する。宣言は自動的にマージされ、コードベース全体が修正の恩恵を受ける。

これらのパターン——厳格な設定、識別共用体、テンプレートリテラル型、ブランド型、satisfies、型安全なジェネリック、明示的なエラーハンドリング——の組み合わせは、2026年のTypeScriptへの包括的なアプローチを表す。各パターンは独立して有用だが、一緒になることで、コンパイラがそうでなければ本番インシデントになるミスをキャッチするコードベースが生まれる。これらのパターンを学ぶための投資は、デバッグ時間の短縮、より安全なリファクタリング、そして真に誤用しにくいAPIとして何倍にもなって報われる。