TypeScript でオブジェクトに型注釈(: 型)を付けると「その型を満たしているか」はチェックできますが、代わりに値の具体的な型情報が失われてしまうことがあります。かといって型注釈を外すと、今度は制約チェックが効きません。この「チェックはしたいが、推論も残したい」というジレンマを解決するのが satisfies 演算子です。この記事では、satisfies の基本構文、型注釈や型アサーション(as)との違い、設定オブジェクトやカラーパレットといったよくある使いどころまで、初心者〜中級者向けに解説します。
目次
satisfies が解決する「チェックか推論か」のジレンマ
まず、satisfies がなぜ必要なのかを具体例で確認しましょう。次のように、キーが文字列・値が文字列か数値のオブジェクトを作りたいとします。型注釈を付けない場合と、付けた場合で何が起きるか見てみます。
// (1) 型注釈なし:具体的な型は残るが、制約チェックが効かない
const config1 = {
host: "localhost",
port: 8080,
};
config1.host.toUpperCase(); // OK(host は string と推論される)
// ただし config1 が「文字列か数値だけ」を満たしているかは検査されない
// (2) 型注釈あり:制約は効くが、具体的な型情報が失われる
const config2: Record<string, string | number> = {
host: "localhost",
port: 8080,
};
config2.host.toUpperCase(); // エラー:string | number に toUpperCase は無い
(2) では config2.host の型が、宣言した string | number にまで広がってしまい、文字列専用の toUpperCase() が呼べなくなっています。制約は守れても、実際には文字列だという情報が消えてしまうわけです。「制約チェック(型が合っているかの検査)」と「具体的な型の推論」を両立できない、これが型注釈の抱えるジレンマです。
satisfies の基本構文
satisfies は、値の後ろに satisfies 型 と書きます。「この値がその型を満たしているか検査してほしい。ただし変数の型は、値そのものの具体的な型のままにしてほしい」という指定です。先ほどの例を satisfies で書き換えると、両方のいいとこ取りができます。
const config = {
host: "localhost",
port: 8080,
} satisfies Record<string, string | number>;
// (A) 制約チェックは効く:型に合わない値はエラーになる
// (B) 具体的な型は保たれる:host は string、port は number のまま
config.host.toUpperCase(); // OK
console.log(config.port * 2); // OK(port は number)
config 全体は Record<string, string | number> を満たしているか検査されつつ、config.host は string、config.port は number と、値どおりの具体的な型に推論されます。型注釈のように string | number へ広がらないため、それぞれの型に応じたメソッドをそのまま呼べます。
型注釈・as との違いを比較する
値の型を指定する方法には、型注釈(: 型)と型アサーション(as 型)、そして satisfies があります。それぞれ「制約チェックをするか」「推論される型がどうなるか」「不正な値をどう扱うか」が異なります。次の表で整理します。
| 書き方 | 制約チェック | 推論される型 | 不正な値のとき |
|---|---|---|---|
const x: T = ...(型注釈) | ある | 宣言した T(具体型が失われる) | エラーになる |
const x = ... satisfies T | ある | 値そのものの具体的な型を保つ | エラーになる |
const x = ... as T(アサーション) | ほぼ無い(握りつぶす) | 指定した T になる | エラーにならない(危険) |
特に as との違いは重要です。as は「コンパイラよ、これはこの型だと信じてくれ」と型を無理やり指定するもので、実際の値が間違っていても黙って通してしまいます。一方 satisfies は逆で、値が型を満たしているかをきちんと検査します。目的が正反対だと考えると分かりやすいでしょう。
type Colors = Record<"primary" | "danger", string>;
// as:キーのタイプミスに気づけない(不正を握りつぶす)
const wrong = {
primary: "#0d6efd",
dangr: "#dc3545", // danger のつもりがタイプミス
} as Colors;
// エラーにならず、wrong.danger は実行時に undefined になる
// satisfies:タイプミスをきちんとエラーにする
const right = {
primary: "#0d6efd",
dangr: "#dc3545",
} satisfies Colors;
// エラー:'danger' がありません/'dangr' は型に存在しない余分なプロパティ
as を使った wrong はコンパイルが通ってしまい、間違いに気づけません。satisfies を使った right は、キーの誤りをその場でエラーとして知らせてくれます。「値が正しいことを保証したい」なら satisfies、というのが基本の使い分けです。
よくある使いどころ
設定オブジェクトやカラーパレット
決まったキーと値を持つ設定オブジェクトやカラーパレットは、satisfies の代表的な使いどころです。「必要なキーがそろっているか」「値の型が合っているか」を検査しつつ、各値の具体的な型を保てます。
type Palette = Record<"primary" | "danger" | "success", string>;
const palette = {
primary: "#0d6efd",
danger: "#dc3545",
success: "#198754",
} satisfies Palette;
// 値は string と分かるので、文字列メソッドがそのまま使える
console.log(palette.primary.toUpperCase()); // "#0D6EFD"
ここで success の指定を忘れたり、値に数値を入れたりすると、その場でエラーになります。それでいて palette.primary は string のままなので、toUpperCase() のような文字列専用の操作も問題なく呼べます。
ルート定義でキーを絞り込む
ページのパスをまとめたルート定義でも satisfies が役立ちます。Record<string, string> で「すべての値が文字列であること」を検査しつつ、キーの一覧(keyof)は具体的なまま保てるため、存在しないキーの参照を型で防げます。
const routes = {
home: "/",
users: "/users",
userDetail: "/users/:id",
} satisfies Record<string, string>;
// キーの一覧が具体的に保たれる
type RouteKey = keyof typeof routes; // "home" | "users" | "userDetail"
function go(key: RouteKey) {
console.log(routes[key]);
}
go("home"); // OK
go("contact"); // エラー:存在しないキーは渡せない
もし routes に Record<string, string> を型注釈として付けてしまうと、keyof がただの string になり、go("contact") のような誤りを検出できなくなります。satisfies なら制約チェックと、キーの絞り込み(推論)を同時に得られます。
使えるバージョンと書ける場所
satisfies 演算子は TypeScript 4.9 以降で使えます。それより前のバージョンでは構文エラーになるため、プロジェクトの TypeScript のバージョンを確認してください。書ける場所は「式の後ろ」で、変数への代入だけでなく、オブジェクトを返す関数の戻り値など式であればどこでも使えます。あくまで型レベルの機能なので、コンパイル後の JavaScript には何も残らず、実行時の動作には影響しません。
まとめ
satisfies 演算子は、値 satisfies 型 と書くことで「値がその型を満たしているか検査しつつ、値そのものの具体的な型(推論)を保つ」ための機能です。型注釈(: 型)は制約チェックはできても具体型が失われ、型アサーション(as)は型を握りつぶして不正な値も通してしまいます。satisfies はその中間で、検査と推論を両立できるのが強みです。設定オブジェクトやカラーパレット、ルート定義のように「決まった形を守りつつ、各値の型も活かしたい」場面で活躍します。TypeScript 4.9 以降で使えるので、対応バージョンなら積極的に取り入れてみてください。