TypeScriptで設定オブジェクトや定数マップを定義するとき、型アノテーションを付けると型推論が消える問題にハマったことはないだろうか。
TypeScript 4.9で導入されたsatisfies演算子を使えば、この問題をエレガントに解決できる。
問題: 型アノテーション vs 型推論のジレンマ
パターン1: 型アノテーションを付ける
type Route = {
path: string
label: string
icon: string
}
// 型アノテーションで型安全にする
const routes: Record<string, Route> = {
home: { path: '/', label: 'ホーム', icon: 'home' },
blog: { path: '/blog', label: 'ブログ', icon: 'book' },
about: { path: '/about', label: '会社概要', icon: 'building' },
}
// NG: キー名の補完が効かない
routes.hoem // ← タイポだが Record<string, Route> なのでエラーにならない
Record<string, Route>と書いた瞬間、キーがstringに広がってタイポを検出できなくなる。
パターン2: 型アノテーションを外す
const routes = {
home: { path: '/', label: 'ホーム', icon: 'home' },
blog: { path: '/blog', label: 'ブログ', icon: 'book' },
about: { path: '/about', label: '会社概要', icon: 'building' },
}
// OK: タイポを検出できる
routes.hoem // ← Property 'hoem' does not exist
// NG: 値の型チェックがない
const broken = {
home: { path: '/', label: 'ホーム' }, // ← icon が抜けてもエラーにならない
}
型推論は効くが、値の構造が正しいかチェックできない。
解決: satisfies 演算子
satisfiesは「この型を満たすことを検証するが、推論は維持する」という演算子。
type Route = {
path: string
label: string
icon: string
}
const routes = {
home: { path: '/', label: 'ホーム', icon: 'home' },
blog: { path: '/blog', label: 'ブログ', icon: 'book' },
about: { path: '/about', label: '会社概要', icon: 'building' },
} satisfies Record<string, Route>
// OK: キー名の補完が効く(推論が維持されている)
routes.hoem // ← Property 'hoem' does not exist ✅
// OK: 値の型チェックも効く
const broken = {
home: { path: '/', label: 'ホーム' }, // ← Property 'icon' is missing ✅
} satisfies Record<string, Route>
型安全と型推論の両立が実現できる。
実務での活用パターン
パターン1: テーマ定数の定義
type ColorToken = {
light: string
dark: string
}
const colors = {
primary: { light: '#3db4ff', dark: '#1a8cd8' },
accent: { light: '#a855f7', dark: '#7c3aed' },
danger: { light: '#ef4444', dark: '#dc2626' },
} satisfies Record<string, ColorToken>
// colors.primary.light は string 型(推論維持)
// colors.priamry とタイポすればエラー
パターン2: APIレスポンスのモック
type User = {
id: number
name: string
email: string
role: 'admin' | 'member' | 'guest'
}
const mockUsers = [
{ id: 1, name: '田中太郎', email: 'tanaka@example.com', role: 'admin' },
{ id: 2, name: '佐藤花子', email: 'sato@example.com', role: 'member' },
] satisfies User[]
// mockUsers[0].role は 'admin' 型(リテラル推論が維持される)
// User[] と型注釈すると 'admin' | 'member' | 'guest' に広がってしまう
パターン3: イベントハンドラーマップ
type EventConfig = {
handler: (payload: unknown) => void
debounce?: number
}
const eventHandlers = {
click: { handler: (p) => console.log('clicked', p) },
scroll: { handler: (p) => console.log('scrolled', p), debounce: 100 },
resize: { handler: (p) => console.log('resized', p), debounce: 200 },
} satisfies Record<string, EventConfig>
// eventHandlers.scrol → エラー(タイポ検出)
// eventHandlers.scroll.debounce → number | undefined(型安全)
satisfies vs as const vs 型アノテーション
// 1. 型アノテーション: 型チェック✅ 推論❌
const a: Record<string, Route> = { home: { path: '/', label: 'ホーム', icon: 'home' } }
// 2. as const: 型チェック❌ 推論✅(readonly化)
const b = { home: { path: '/', label: 'ホーム', icon: 'home' } } as const
// 3. satisfies: 型チェック✅ 推論✅
const c = { home: { path: '/', label: 'ホーム', icon: 'home' } } satisfies Record<string, Route>
// 4. as const + satisfies: 型チェック✅ 推論✅ + readonly化✅
const d = { home: { path: '/', label: 'ホーム', icon: 'home' } } as const satisfies Record<string, Route>
| 方法 | 型チェック | キー推論 | リテラル推論 | readonly |
|---|---|---|---|---|
| 型アノテーション | ✅ | ❌ | ❌ | ❌ |
| as const | ❌ | ✅ | ✅ | ✅ |
| satisfies | ✅ | ✅ | ✅ | ❌ |
| as const satisfies | ✅ | ✅ | ✅ | ✅ |
注意点
1. satisfies は代入時のみ使える
// OK
const x = { a: 1 } satisfies Record<string, number>
// NG: 関数の引数には使えない
// someFunction({ a: 1 } satisfies SomeType) // 構文的にはOKだが冗長
関数の引数にはそもそも型チェックが効くので、satisfiesは定数定義に使うのが自然。
2. TypeScript 4.9 以降が必要
プロジェクトのTypeScriptバージョンを確認してから使おう。
npx tsc --version
4.9未満の場合は、型アノテーション + ヘルパー関数で代替できる:
// satisfies の代替(4.9未満)
function defineRoutes<T extends Record<string, Route>>(routes: T): T {
return routes
}
const routes = defineRoutes({
home: { path: '/', label: 'ホーム', icon: 'home' },
})
// routes.home.path → string(推論維持 + 型チェック)
まとめ
| 場面 | 推奨 |
|---|---|
| 設定オブジェクト・定数マップ | satisfies |
| 完全にイミュータブルな定数 | as const satisfies |
| 関数の引数 | 通常の型アノテーション |
| 型推論が不要な場合 | 通常の型アノテーション |
satisfiesは「型安全にしたいが、推論も失いたくない」という日常的なジレンマを解決する。特に設定ファイルや定数マップの定義で効果を発揮するので、積極的に使っていこう。
