Back to blog
|4 min read

実務で役立つTypeScriptパターン5選

普段の開発で実際に使っているTypeScriptの型テクニックやパターンを紹介。Discriminated Union、Branded Type、satisfies演算子など。

TypeScript
Tips

はじめに

TypeScript を書いていると「もっと型で守れないかな」と思うことがよくあります。この記事では、実務で実際に使っているパターンを5つ紹介します。

1. Discriminated Union で状態を安全に管理する

API レスポンスやUIの状態管理で特に使えるパターンです。

type ApiResult<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }

function handleResult(result: ApiResult<User>) {
  switch (result.status) {
    case 'loading':
      return <Spinner />
    case 'success':
      // result.data に安全にアクセスできる
      return <UserCard user={result.data} />
    case 'error':
      // result.error に安全にアクセスできる
      return <ErrorMessage message={result.error} />
  }
}

status をキーにした分岐で、TypeScript が自動的に型を絞り込んでくれます。if-else でフラグを使うよりずっと安全。

2. Branded Type で ID の取り違えを防ぐ

string 型の ID を複数扱うとき、取り違えが起きがちです。

type UserId = string & { readonly __brand: 'UserId' }
type TeamId = string & { readonly __brand: 'TeamId' }

const createUserId = (id: string): UserId => id as UserId
const createTeamId = (id: string): TeamId => id as TeamId

function getUser(id: UserId): Promise<User> { /* ... */ }

const userId = createUserId('user-123')
const teamId = createTeamId('team-456')

getUser(userId) // OK
getUser(teamId) // コンパイルエラー!

ランタイムのコストはゼロ。型チェックだけで取り違えを防げます。

3. satisfies 演算子で型推論を維持する

as const と組み合わせると、リテラル型を維持しつつ型チェックもできます。

type Route = {
  path: string
  label: string
}

// ❌ as const だけだと Route 型のチェックがない
const routes1 = [
  { path: '/home', label: 'Home' },
  { path: '/about', label: 'About' },
] as const

// ✅ satisfies で型チェック + リテラル型も維持
const routes = [
  { path: '/home', label: 'Home' },
  { path: '/about', label: 'About' },
] as const satisfies readonly Route[]

// routes[0].path は "/home" リテラル型

satisfies は TypeScript 4.9 以降で使えます。古いプロジェクトでは注意。

4. Template Literal Types でパターンマッチ

イベント名やAPI パスなど、特定のパターンに従う文字列を型で縛れます。

type EventName = `on${Capitalize<string>}`

function addEventListener(event: EventName, handler: () => void) {
  // ...
}

addEventListener('onClick', handleClick)   // OK
addEventListener('click', handleClick)     // エラー!"on" で始まらない

API のエンドポイントにも使えます。

type ApiPath = `/api/${string}`

async function fetchApi(path: ApiPath): Promise<Response> {
  return fetch(path)
}

fetchApi('/api/users')    // OK
fetchApi('/users')        // エラー!

5. Conditional Types でユーティリティを作る

既存の型から新しい型を導出するのに使います。

// ネストしたオブジェクトを全て Optional にする
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T

type Config = {
  database: {
    host: string
    port: number
    credentials: {
      username: string
      password: string
    }
  }
}

// 全てのプロパティが Optional になる
type PartialConfig = DeepPartial<Config>

実務では設定ファイルのマージやフォーム初期値で重宝します。

まとめ

パターンユースケース
Discriminated Union状態管理、API レスポンス
Branded TypeID の取り違え防止
satisfies型チェック + 型推論の維持
Template Literal Typesパターンに従う文字列の型安全
Conditional Typesユーティリティ型の作成

TypeScript の型システムは奥が深いですが、これらのパターンを知っておくだけでコードの安全性がかなり上がります。

次回は AI 系のツール開発について書く予定です。