メインコンテンツまでスキップ

satisfies演算子 (satisfies operator)

satisfiesは、「型に合っているかだけ検査して、推論結果はそのまま残す」ための演算子です。型注釈が持つ「型安全性の保証」と、型推論が持つ「具体的な値の使いやすさ」の、いいとこ取りができるのが特徴です。

型注釈と型推論のトレードオフ

satisfiesが登場する前は、「安全性を取るか」「使い勝手を取るか」のトレードオフがありました。

次のような構造の設定オブジェクトの例で考えてみます。Config型はテキストサイズを定義する型で、文字列リテラルまたは数値を受け入れます。

ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};

型チェックをしないと危険

まず、設定オブジェクトを型注釈なしで定義した例を見てみましょう。以下は設定値を誤ってしまったケースです。

ts
// 型注釈をつけていない
const config = { textSize: "extra-large" };
const config: { textSize: string; }
ts
// 型注釈をつけていない
const config = { textSize: "extra-large" };
const config: { textSize: string; }

configの型は{ textSize: string }と推論され、Config型とは合致していないことがわかります。textSize"small" | "medium" | "large" | numberのいずれかであるべきで、任意の文字列(string)は入れられないはずです。

本来なら、Config型の定義にしたがってextra-largeという値の代入を防ぎたいところですが、型注釈をつけていないためコンパイラによるチェックが行われません。

型注釈すると安全にはなるが…

コンパイラにチェックしてもらうために、型注釈をつけてみましょう。

ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
const config: Config = { textSize: "extra-large" };
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.
ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
const config: Config = { textSize: "extra-large" };
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.

型注釈をつけたのでチェックが効き、使ってはいけないextra-largeがあぶり出されました。これで解決したように見えますが、新たな問題が発生します。

推論は消えてしまう

型注釈をつけるとチェックは効くようになりますが、副作用として推論結果が変わってしまいます。

ts
// 型注釈がないため推論あり
const config1 = { textSize: 10 };
const config1: { textSize: number; }
config1.textSize;
(property) textSize: number
// 型注釈があるため推論なし
const config2: Config = { textSize: 10 };
const config2: Config
config2.textSize;
(property) textSize: number | "small" | "medium" | "large"
ts
// 型注釈がないため推論あり
const config1 = { textSize: 10 };
const config1: { textSize: number; }
config1.textSize;
(property) textSize: number
// 型注釈があるため推論なし
const config2: Config = { textSize: 10 };
const config2: Config
config2.textSize;
(property) textSize: number | "small" | "medium" | "large"

config1{ textSize: number }と推論されましたが、config2Config型になっています。
そのため、config1.textSizenumber型ですが、config2.textSize"small" | "medium" | "large" | number型です。このように、型推論と型注釈では得られる型情報が異なります。

別の見方をすると、{ textSize: 10 }config2に代入された時点で、コンパイラが「textSizenumberだったこと」を忘れてしまうとも言えます。

これで困るのは、textSizeが数値であることを前提にした処理を直接書けなくなることです。

ts
const config: Config = { textSize: 10 };
const configForMobile: Config = {
textSize: config.textSize * 1.1,
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.2362The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
};
ts
const config: Config = { textSize: 10 };
const configForMobile: Config = {
textSize: config.textSize * 1.1,
The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.2362The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
};

人間がコードを読む限り、textSizeが数値であることは明らかですが、コンパイラはtextSizeに数値以外が入る可能性まで考慮してしまい、textSize * 1.1の計算式をエラーとして報告してきます。

これを回避するには、次のように型ガードを書かなければなりません。textSize10であることは自明であるにもかかわらず、です。

ts
const config: Config = { textSize: 10 };
if (typeof config.textSize !== "number") {
throw new Error("textSize is not number");
}
const configForMobile: Config = { textSize: config.textSize * 1.1 };
ts
const config: Config = { textSize: 10 };
if (typeof config.textSize !== "number") {
throw new Error("textSize is not number");
}
const configForMobile: Config = { textSize: config.textSize * 1.1 };

もしくは、型アサーションを使う方法もありますが、記述が冗長になってしまいます。

ts
const config: Config = { textSize: 10 };
const configForMobile: Config = {
textSize: (config as { textSize: number }).textSize * 1.1,
};
ts
const config: Config = { textSize: 10 };
const configForMobile: Config = {
textSize: (config as { textSize: number }).textSize * 1.1,
};

このように、型注釈を外せば型推論が効いて数値計算を直接書けるようになりますが、型チェックがなくなるので設定ミスに気づきにくくなります。逆に、型注釈があると設定ミスは防げますが、数値計算を直接行うことができなくなります。

型チェックと型推論の両立

「型チェックはしたいが、中身がnumberであるという情報は消さないでほしい」この願いを叶えるのがsatisfiesです。上の例をsatisfiesで解決してみましょう。

ts
const config = { textSize: 10 } satisfies Config;
ts
const config = { textSize: 10 } satisfies Config;

主な変更点は、configの型注釈を外し、値の後ろにsatisfies Configをつけたことです。

これにより、{ textSize: 10 }Config型に丸められることなく、{ textSize: number }として推論されます。

ts
const config = { textSize: 10 } satisfies Config;
const config: { textSize: number; }
ts
const config = { textSize: 10 } satisfies Config;
const config: { textSize: number; }

そして、textSize * 1.1の計算式もエラーとして報告されなくなります。

ts
const config = { textSize: 10 } satisfies Config;
const configForMobile: Config = {
textSize: config.textSize * 1.1, // OK
};
ts
const config = { textSize: 10 } satisfies Config;
const configForMobile: Config = {
textSize: config.textSize * 1.1, // OK
};

また、設定できない値はチェックされるので安全性も保証されます。つまり、設定ミスを防ぐことができます。

ts
const config = { textSize: "extra-large" } satisfies Config;
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.
ts
const config = { textSize: "extra-large" } satisfies Config;
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.

比較

型注釈、型推論、satisfiesの違いをまとめると次のようになります。

特徴型注釈なし(型推論任せ)型注釈ありsatisfies
型安全性❌ チェックなし✅ チェックあり✅ チェックあり
具体的な値の情報✅ 保持❌ 消える✅ 保持

実用的なユースケース

satisfiesの具体的なユースケースを見てみましょう。

設定ファイル

前述の例と同様に、アプリケーションの設定をTypeScriptで記述する場合、satisfiesが最適です。テーマ設定、デザイントークン、CLIツールの設定ファイルなどがこれに該当します。

式の中でのインライン型チェック

変数を宣言せずに、その場で型チェックだけを行いたい場合にも便利です。

たとえば、次のようにJSON.stringifyにオブジェクトリテラルを直接渡すようなケースです。

ts
type User = { id: number; name: string };
 
// idプロパティが足りないことに気づける
JSON.stringify({ name: "taro" } satisfies User);
Type '{ name: string; }' does not satisfy the expected type 'User'. Property 'id' is missing in type '{ name: string; }' but required in type 'User'.1360Type '{ name: string; }' does not satisfy the expected type 'User'. Property 'id' is missing in type '{ name: string; }' but required in type 'User'.
 
// satisfiesがないと、idプロパティが足りないことに気づけない
JSON.stringify({ name: "taro" });
ts
type User = { id: number; name: string };
 
// idプロパティが足りないことに気づける
JSON.stringify({ name: "taro" } satisfies User);
Type '{ name: string; }' does not satisfy the expected type 'User'. Property 'id' is missing in type '{ name: string; }' but required in type 'User'.1360Type '{ name: string; }' does not satisfy the expected type 'User'. Property 'id' is missing in type '{ name: string; }' but required in type 'User'.
 
// satisfiesがないと、idプロパティが足りないことに気づけない
JSON.stringify({ name: "taro" });

もしsatisfiesを使わないとなると、型チェックのために一度変数に入れて型注釈を書く必要があり、コードの形も変えなければなりません。

ts
type User = { id: number; name: string };
const user: User = { id: 1, name: "taro" };
const json = JSON.stringify(user);
ts
type User = { id: number; name: string };
const user: User = { id: 1, name: "taro" };
const json = JSON.stringify(user);

似た例として、default exportの値をチェックしたい場合にも便利です。

ts
type Config = { textSize: "small" | "medium" | "large" | number };
 
// "extra-large"が設定ミスであることに気づける
export default { textSize: "extra-large" } satisfies Config;
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.
ts
type Config = { textSize: "small" | "medium" | "large" | number };
 
// "extra-large"が設定ミスであることに気づける
export default { textSize: "extra-large" } satisfies Config;
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.

もしsatisfiesを使わないとなると、型チェックのために一度変数に入れる必要があります。

ts
type Config = { textSize: "small" | "medium" | "large" | number };
const config: Config = { textSize: "extra-large" };
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.
export default config;
ts
type Config = { textSize: "small" | "medium" | "large" | number };
const config: Config = { textSize: "extra-large" };
Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.
export default config;

constアサーションとの組み合わせ

constアサーション(as const)は、値を変更不可(readonly)かつリテラル型として扱うようTypeScriptコンパイラに指示する機能です。これとsatisfiesを組み合わせると、値を厳格なリテラル型として固定しつつ、型チェックも行うことができます。

ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
// constアサーションを使わない場合
const config1 = { textSize: 10 } satisfies Config;
const config1: { textSize: number; }
// constアサーションを使った場合
const config2 = { textSize: 10 } as const satisfies Config;
const config2: { readonly textSize: 10; }
ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
// constアサーションを使わない場合
const config1 = { textSize: 10 } satisfies Config;
const config1: { textSize: number; }
// constアサーションを使った場合
const config2 = { textSize: 10 } as const satisfies Config;
const config2: { readonly textSize: 10; }

config1{ textSize: number }と推論されますが、config2{ readonly textSize: 10 }と推論されます。違いとしてtextSizenumber型よりも具体的な10(リテラル型)になっていることがわかります。

ソースコードには10と書いてあるので、よりコード通りの情報を型に持たせられていると言えます。number型よりも具体的な情報が保持されることで、型情報を用いた発展的な処理につなげていくことができます。

少し応用的な例になりますが、テンプレートリテラル型と組み合わせた例を見てみます。configAas constなし)は単なるnumberなので推論結果が"${number}px"になりますが、configBas constあり)は10という値を持っているので"${10}px"(つまり"10px")という厳密な型を導き出せます。

ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
const configA = { textSize: 10 } satisfies Config;
const configB = { textSize: 10 } as const satisfies Config;
 
// Config.textSizeの数値から"[数値]px"というリテラル型を導出する型レベル関数
type Px<T extends Config> = `${T["textSize"]}px`;
 
// constアサーションなしのほうは [数字]px という文字列が代入可能
const fontSize1: Px<typeof configA> = "10px"; // OK
const fontSize2: Px<typeof configA> = "999px"; // OK
 
// constアサーションありのほうは "10px" という文字列だけに限定できる
const fontSize3: Px<typeof configB> = "10px"; // OK
const fontSize4: Px<typeof configB> = "999px"; // エラー
Type '"999px"' is not assignable to type '"10px"'.2322Type '"999px"' is not assignable to type '"10px"'.
ts
type Config = {
textSize: "small" | "medium" | "large" | number;
};
const configA = { textSize: 10 } satisfies Config;
const configB = { textSize: 10 } as const satisfies Config;
 
// Config.textSizeの数値から"[数値]px"というリテラル型を導出する型レベル関数
type Px<T extends Config> = `${T["textSize"]}px`;
 
// constアサーションなしのほうは [数字]px という文字列が代入可能
const fontSize1: Px<typeof configA> = "10px"; // OK
const fontSize2: Px<typeof configA> = "999px"; // OK
 
// constアサーションありのほうは "10px" という文字列だけに限定できる
const fontSize3: Px<typeof configB> = "10px"; // OK
const fontSize4: Px<typeof configB> = "999px"; // エラー
Type '"999px"' is not assignable to type '"10px"'.2322Type '"999px"' is not assignable to type '"10px"'.

このようにas constsatisfiesの合わせ技は、より厳密な型定義が必要な場面で強力な味方になってくれるでしょう。