TypeScriptの satisfies 演算子を使いこなす ─ 型推論を壊さずに型チェックする方法
TypeScript を書いていて、こんな経験ありませんか。
オブジェクトに型アノテーションをつけたら、推論が効かなくなって .toUpperCase() が使えなくなった。as で型アサーションしたら、間違った値を書いても怒られなくなった...。
そういう「型をつけたいけど、推論も残したい」という場面に、 satisfies 演算子 がかなりハマります。TypeScript 4.9 で登場したのですが、正直まだあまり使われていない気がします。もったいないなと思って書きました。
この記事で学べること:
-
satisfiesが何をするものか(30秒で理解できる説明) - 型アノテーション・
as const・asとの違い -
satisfiesが活きる実践的な3パターン - よくある落とし穴と使い分けの判断基準
検証環境: TypeScript 5.x
satisfies が解決する問題
まず問題から入ります。
こういうコードを書いたことがあると思います。
// 設定オブジェクトに型をつけたい
type Config = {
endpoint: string | URL;
timeout: number;
};
const config: Config = {
endpoint: "https://api.example.com",
timeout: 3000,
};
// ここで詰まる
config.endpoint.toUpperCase(); // ❌ エラー: string | URL には toUpperCase がない
Config 型では endpoint が string | URL なので、TypeScript は「文字列かもしれないし URL かもしれない」と判断して .toUpperCase() を許してくれません。でも実際には文字列を入れているのにもったいないですよね。
かといって型アノテーションを外すと、型チェックが効かなくなります。
// 型アノテーションなし → 推論は効くが型チェックがない
const config = {
endpoint: "https://api.example.com",
timeout: 3000,
};
config.endpoint.toUpperCase(); // ✅ 動く
config.endpoint = 12345; // ❌ 怒ってほしいのに怒ってくれない...
この「 型チェックはしたいけど、推論も失いたくない 」というジレンマを解決するのが satisfies です。
satisfies を使うとこうなる
type Config = {
endpoint: string | URL;
timeout: number;
};
const config = {
endpoint: "https://api.example.com",
timeout: 3000,
} satisfies Config;
// 両方いける
config.endpoint.toUpperCase(); // ✅ 推論が効いて string として扱われる
config.endpoint = 12345; // ❌ 型エラー(Config の制約が効いている)
satisfies Config と書くことで、「このオブジェクトは Config 型を満たしているか検証してほしい(でも型は推論したものを使ってほしい)」という意味になります。
一言で言うなら、 「型チェックだけして、型の上書きはしない」 という演算子です。
型アノテーション・as const・as との比較
少し整理してみます。
型アノテーション(: Type)
const config: Config = { endpoint: "https://...", timeout: 3000 };
// config.endpoint の型 → string | URL(Config の定義に引っ張られる)
型チェックは効く。でも変数の型が Config の定義に上書きされるので、推論が失われます。
as const
const config = { endpoint: "https://...", timeout: 3000 } as const;
// config.endpoint の型 → "https://..."(リテラル型)
// config.timeout の型 → 3000(リテラル型)
推論は最大限効く。でも Config を満たしているかのチェックはしてくれません。
as(型アサーション)
const config = { endpoint: 123, timeout: "hello" } as Config;
// ❌ 明らかにおかしくても通ってしまう
型チェックをほぼバイパスします。緊急回避以外では使わないほうがいいです。
satisfies
const config = { endpoint: "https://...", timeout: 3000 } satisfies Config;
// config.endpoint の型 → string(推論が効いている)
// Config を満たしているかチェック → ✅
型チェックと推論を両立します。
| 型チェック | 推論を保持 | |
|---|---|---|
: Type |
✅ | ❌ |
as const |
❌ | ✅(リテラル) |
as |
△(緩い) | △ |
satisfies |
✅ | ✅ |
satisfies が活きる実践パターン
パターン1: 設定オブジェクトの型安全管理
アプリの設定値を型チェックしながら、各プロパティの具体的な型も使いたい場面。
type AppConfig = {
api: {
baseUrl: string;
timeout: number;
};
features: {
darkMode: boolean;
language: "ja" | "en";
};
};
const config = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
},
features: {
darkMode: true,
language: "ja",
},
} satisfies AppConfig;
// 推論が効いているのでメソッドが使える
const upperUrl = config.api.baseUrl.toUpperCase(); // ✅
const doubled = config.api.timeout * 2; // ✅
// Config を満たさない値はエラー
// config.features.language = "fr"; // ❌ "ja" | "en" 以外はエラー
パターン2: Record 型のオブジェクトで各値の型を保持する
type RouteConfig = {
path: string;
method: "GET" | "POST" | "PUT" | "DELETE";
};
const routes = {
getUser: { path: "/users/:id", method: "GET" },
createUser: { path: "/users", method: "POST" },
updateUser: { path: "/users/:id", method: "PUT" },
} satisfies Record<string, RouteConfig>;
// satisfies なしだと routes.getUser の型が RouteConfig になって推論が失われる
// satisfies ありだと各プロパティの型が保持される
// routes.getUser.method の型 → "GET"(リテラル型として推論される)
// routes.createUser.method の型 → "POST"
これが特に便利なのは、 ユニオン型を絞り込んだあとで使いたい 場面です。
function handleRoute(route: typeof routes.getUser) {
// method の型が "GET" として絞り込まれている
console.log(route.method); // 型: "GET"
}
型アノテーション(: Record<string, RouteConfig>)を使った場合、routes.getUser.method の型は "GET" | "POST" | "PUT" | "DELETE" になってしまいます。satisfies だと "GET" のまま保持されます。
パターン3: switch 文の網羅性チェックと組み合わせる
type Status = "pending" | "running" | "done" | "error";
type StatusMessage = {
label: string;
color: "gray" | "blue" | "green" | "red";
};
// Status の全パターンを網羅しているかチェックしたい
const statusMessages = {
pending: { label: "処理待ち", color: "gray" },
running: { label: "処理中", color: "blue" },
done: { label: "完了", color: "green" },
error: { label: "エラー", color: "red" },
} satisfies Record<Status, StatusMessage>;
// Status に新しい値を追加したとき、ここでエラーになる → 網羅性の保証
// 例: Status に "cancelled" を追加すると、このオブジェクトが型エラーになる
これはかなり実用的です。 ステータスコードや enum 的なものの対応表を管理するとき 、satisfies を使っておくと型の追加漏れをコンパイルエラーで検知できます。
よくある落とし穴
推論が思ったより広くなることがある
type Shape = {
kind: "circle" | "square";
size: number;
};
const shape = { kind: "circle", size: 10 } satisfies Shape;
// shape.kind の型 → "circle"(推論)
// ただし Shape を経由しているので、"circle" | "square" の範囲内であることは保証される
satisfies を使っても、推論される型は実際の値から決まります。これは意図通りの挙動です。
as const satisfies の組み合わせ
// リテラル型で固定しつつ、型チェックも効かせたい場合
const config = {
endpoint: "https://api.example.com",
timeout: 3000,
} as const satisfies Config;
// config.endpoint の型 → "https://api.example.com"(リテラル型)
// Config を満たしているかチェック → ✅
as const satisfies の順番は、 as const が先 です。satisfies as const は文法エラーになります。
使い分けのまとめ
正直なところ、最初は「どこで使うのか」がわかりにくいと思います。判断基準をシンプルにまとめると...
satisfies を使う場面:
- オブジェクトの各プロパティの推論を保持したまま、型の整合性をチェックしたい
-
Record<K, V>形式のオブジェクトで、各値の具体的な型を使いたい - ユニオン型の全パターンを網羅しているか保証したい
型アノテーション(: Type)を使う場面:
- 変数の型を明示的に広くしておきたい(柔軟に再代入したい)
- 推論の具体的な型よりも、インターフェースの型を使いたい
as const を使う場面:
- 値をリテラル型として固定したい(設定値の変更不可化)
設定オブジェクト、ルート定義、ステータスマッピングなど「 型チェックはしたいが、各プロパティの型も使いたい 」という場面が出てきたら、まず satisfies を試してみると良いと思います。
まとめ
satisfies を一行で表すなら、 「型チェックだけして、型の上書きはしない」 です。
型アノテーションを使ったら推論が消えてしまった、as を使ったらチェックが効かなくなった、そういう経験のある人には刺さると思います。
TypeScript の型システムは年々豊かになっていて、「こう書けばいいのに」という気持ちに応えてくれる機能が少しずつ増えています。satisfies もそのひとつです。知っておくと、型と推論の両立で悩む回数が減る気がします。
// こういうジレンマを感じたら satisfies のことを思い出してください
const config = {
endpoint: "https://api.example.com",
timeout: 3000,
} satisfies Config; // 型チェック ✅ 推論も ✅