satisfies演算子で型チェックと推論を両立する
satisfies演算子で型チェックと推論を両立する
設定オブジェクトを定義するとき、「型は守りたいけど、各プロパティの具体的な値の情報も残したい」という場面でよく困っていました。型注釈を付けると推論が広がってしまうし、かといって as で縛るとチェックが効かなくなる。そのあいだで困ったときのメモです。
結論から言うと、TypeScript 4.9 で入った satisfies 演算子を使えば、型に合っているかのチェックはしつつ、推論された具体的な型はそのまま残せます。最初に見たときは「これがほしかったやつだ」となりました。
まず何が問題なのか
たとえば、カラーパレットをオブジェクトで持つとします。値は文字列だったり、RGBの配列だったりするとします。
type RGB = [number, number, number]
const palette = {
primary: '#0ea5e9',
danger: [255, 0, 0],
}
このままだと型は推論まかせで、palette のキーをタイポしても気づけません。そこで型注釈を付けたくなります。
type Palette = Record<string, string | RGB>
const palette: Palette = {
primary: '#0ea5e9',
danger: [255, 0, 0],
}
これでキーと値の形はチェックされます。ですが今度は別の問題が出ます。
palette.primary.toUpperCase() // エラー:string | RGB には toUpperCase がない
palette.primary は人間には文字列だと分かっているのに、型のうえでは string | RGB に広がってしまいました。注釈を付けた瞬間、せっかくの具体的な推論が消えてしまうわけです。これが地味に不便でした。
as で縛るとチェックが効かなくなる
「じゃあ as で型を当てればいいのでは」と思って、こう書いていた時期もあります。
const palette = {
primary: '#0ea5e9',
danger: [255, 0, 0],
} as Record<string, string | RGB>
これは推論の問題は残りますし、そもそも as は 「合っていることにしてくれ」とコンパイラに伝える操作 です。チェックを通すための小細工なので、本来検出したいミスを見逃します。
const palette = {
primary: '#0ea5e9',
danger: [0, 0], // 本当は要素3つなのに、as だと通ってしまうことがある
} as Record<string, string | RGB>
as は最後の手段で、できれば避けたいやつです。
satisfies なら両立する
ここで satisfies の出番です。式 satisfies 型 と書くと、「この値はこの型を満たしているか」をチェックしつつ、変数の型は推論されたままにします。
type RGB = [number, number, number]
type Palette = Record<string, string | RGB>
const palette = {
primary: '#0ea5e9',
danger: [255, 0, 0],
} satisfies Palette
palette.primary.toUpperCase() // OK:primary は string と推論されている
palette.danger[0] // OK:danger は RGB([number, number, number])
注釈のときと違って、palette.primary はちゃんと string のまま扱えます。それでいて、Palette を満たしていないとエラーになります。
const palette = {
primary: '#0ea5e9',
danger: [255, 0], // エラー:要素が足りない=RGB を満たさない
} satisfies Palette
「型に合っているかは確認したい。でも具体的な型情報は捨てたくない」という、まさに最初の悩みがそのまま解けました。
キーの補完も効く
satisfies のうれしい副作用として、キーの集合が推論で確定する のでキー名の補完が効きます。Record<string, ...> で注釈すると、キーは任意の文字列扱いになってしまうので、ここは差が出ます。
const routes = {
home: '/',
about: '/about',
contact: '/contact',
} satisfies Record<string, string>
// home / about / contact が補完される。タイポはエラーになる
const path = routes.about
注釈版だと routes.aboutt のようなタイポでも string | undefined で素通りしがちですが、satisfies 版なら存在しないキーとして弾けます。
as const と組み合わせる
値そのものをリテラル型として固定したいときは、as const と併用すると効きます。as const で値を固定しつつ、satisfies で「想定した形に収まっているか」を確認する、という役割分担です。
const config = {
env: 'production',
retries: 3,
} as const satisfies {
env: 'development' | 'production'
retries: number
}
config.env // 型は 'production'(リテラル)のまま
env が 'development' | 'production' のどちらかであることはチェックしつつ、実際の値は 'production' というリテラル型で残ります。as const 単体だと型の制約はかけられないので、ここは satisfies が補ってくれます。リテラル型の話はas constの記事も合わせて読むと分かりやすいと思います。
どう使い分けるか
ざっくり、こんな整理で考えています。
- 値を渡す前に 形だけ確認したい(推論は残したい)→
satisfies - 関数の引数や戻り値など、インターフェイスを固定したい → これまで通り型注釈
- どうしても型が合わないのを承知で通したい →
as(最後の手段)
設定オブジェクトやマッピングのように「具体的な値の情報が後で効いてくる」ものは satisfies、API境界のように「型を契約として固定したい」ものは注釈、という感じで使い分けるとよさそうです。
まとめ
- 型注釈は型を固定する代わりに、推論された具体的な型を広げてしまう
asはチェックを素通りさせる小細工なので、本来検出したいミスを見逃すsatisfiesは 型チェックを効かせつつ、推論された型をそのまま残すas const satisfies 型で「値はリテラルのまま・形は検証」も両立できる
satisfies を知ってからは、設定まわりで as を書く機会がぐっと減りました。詳しい挙動はTypeScript公式ハンドブック(The satisfies Operator)も見つつ、まずは手元の設定オブジェクトで試してみるとよさそうです。以上です。