satisfies演算子 (satisfies operator)
satisfiesは、「型に合っているかだけ検査して、推論結果はそのまま残す」ための演算子です。型注釈が持つ「型安全性の保証」と、型推論が持つ「具体的な値の使いやすさ」の、いいとこ取りができるのが特徴です。
型注釈と型推論のトレードオフ
satisfiesが登場する前は、「安全性を取るか」「使い勝手を取るか」のトレードオフがありました。
次のような構造の設定オブジェクトの例で考えてみます。Config型はテキストサイズを定義する型で、文字列リテラルまたは数値を受け入れます。
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};
型チェックをしないと危険
まず、設定オブジェクトを型注釈なしで定義した例を見てみましょう。以下は設定値を誤ってしまったケースです。
ts// 型注釈をつけていないconstconfig = {textSize : "extra-large" };
ts// 型注釈をつけていないconstconfig = {textSize : "extra-large" };
configの型は{ textSize: string }と推論され、Config型とは合致していないことがわかります。textSizeは"small" | "medium" | "large" | numberのいずれかであるべきで、任意の文字列(string)は入れられないはずです。
本来なら、Config型の定義にしたがってextra-largeという値の代入を防ぎたいところですが、型注釈をつけていないためコンパイラによるチェックが行われません。
型注釈すると安全にはなるが…
コンパイラにチェックしてもらうために、型注釈をつけてみましょう。
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};constType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config :Config = {: "extra-large" }; textSize
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};constType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config :Config = {: "extra-large" }; textSize
型注釈をつけたのでチェックが効き、使ってはいけないextra-largeがあぶり出されました。これで解決したように見えますが、新たな問題が発生します。
推論は消えてしまう
型注釈をつけるとチェックは効くようになりますが、副作用として推論結果が変わってしまいます。
ts// 型注釈がないため推論ありconstconfig1 = {textSize : 10 };config1 .textSize ;// 型注釈があるため推論なしconstconfig2 :Config = {textSize : 10 };config2 .textSize ;
ts// 型注釈がないため推論ありconstconfig1 = {textSize : 10 };config1 .textSize ;// 型注釈があるため推論なしconstconfig2 :Config = {textSize : 10 };config2 .textSize ;
config1は{ textSize: number }と推論されましたが、config2はConfig型になっています。
そのため、config1.textSizeはnumber型ですが、config2.textSizeは"small" | "medium" | "large" | number型です。このように、型推論と型注釈では得られる型情報が異なります。
別の見方をすると、{ textSize: 10 }はconfig2に代入された時点で、コンパイラが「textSizeがnumberだったこと」を忘れてしまうとも言えます。
これで困るのは、textSizeが数値であることを前提にした処理を直接書けなくなることです。
tsconstconfig :Config = {textSize : 10 };constconfigForMobile :Config = {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 :config .textSize * 1.1,};
tsconstconfig :Config = {textSize : 10 };constconfigForMobile :Config = {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 :config .textSize * 1.1,};
人間がコードを読む限り、textSizeが数値であることは明らかですが、コンパイラはtextSizeに数値以外が入る可能性まで考慮してしまい、textSize * 1.1の計算式をエラーとして報告してきます。
これを回避するには、次のように型ガードを書かなければなりません。textSizeが10であることは自明であるにもかかわらず、です。
tsconstconfig :Config = {textSize : 10 };if (typeofconfig .textSize !== "number") {throw newError ("textSize is not number");}constconfigForMobile :Config = {textSize :config .textSize * 1.1 };
tsconstconfig :Config = {textSize : 10 };if (typeofconfig .textSize !== "number") {throw newError ("textSize is not number");}constconfigForMobile :Config = {textSize :config .textSize * 1.1 };
もしくは、型アサーションを使う方法もありますが、記述が冗長になってしまいます。
tsconstconfig :Config = {textSize : 10 };constconfigForMobile :Config = {textSize : (config as {textSize : number }).textSize * 1.1,};
tsconstconfig :Config = {textSize : 10 };constconfigForMobile :Config = {textSize : (config as {textSize : number }).textSize * 1.1,};
このように、型注釈を外せば型推論が効いて数値計算を直接書けるようになりますが、型チェックがなくなるので設定ミスに気づきにくくなります。逆に、型注釈があると設定ミスは防げますが、数値計算を直接行うことができなくなります。
型チェックと型推論の両立
「型チェックはしたいが、中身がnumberであるという情報は消さないでほしい」この願いを叶えるのがsatisfiesです。上の例をsatisfiesで解決してみましょう。
tsconstconfig = {textSize : 10 } satisfiesConfig ;
tsconstconfig = {textSize : 10 } satisfiesConfig ;
主な変更点は、configの型注釈を外し、値の後ろにsatisfies Configをつけたことです。
これにより、{ textSize: 10 }はConfig型に丸められることなく、{ textSize: number }として推論されます。
tsconstconfig = {textSize : 10 } satisfiesConfig ;
tsconstconfig = {textSize : 10 } satisfiesConfig ;
そして、textSize * 1.1の計算式もエラーとして報告されなくなります。
tsconstconfig = {textSize : 10 } satisfiesConfig ;constconfigForMobile :Config = {textSize :config .textSize * 1.1, // OK};
tsconstconfig = {textSize : 10 } satisfiesConfig ;constconfigForMobile :Config = {textSize :config .textSize * 1.1, // OK};
また、設定できない値はチェックされるので安全性も保証されます。つまり、設定ミスを防ぐことができます。
tsconstType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config = {: "extra-large" } satisfies textSize Config ;
tsconstType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config = {: "extra-large" } satisfies textSize Config ;
比較
型注釈、型推論、satisfiesの違いをまとめると次のようになります。
| 特徴 | 型注釈なし(型推論任せ) | 型注釈あり | satisfies |
|---|---|---|---|
| 型安全性 | ❌ チェックなし | ✅ チェックあり | ✅ チェックあり |
| 具体的な値の情報 | ✅ 保持 | ❌ 消える | ✅ 保持 |
実用的なユースケース
satisfiesの具体的なユースケースを見てみましょう。
設定ファイル
前述の例と同様に、アプリケーションの設定をTypeScriptで記述する場合、satisfiesが最適です。テーマ設定、デザイントークン、CLIツールの設定ファイルなどがこれに該当します。
式の中でのインライン型チェック
変数を宣言せずに、その場で型チェックだけを行いたい場合にも便利です。
たとえば、次のようにJSON.stringifyにオブジェクトリテラルを直接渡すようなケースです。
tstypeUser = {id : number;name : string };// idプロパティが足りないことに気づける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'.JSON .stringify ({name : "taro" }satisfies User );// satisfiesがないと、idプロパティが足りないことに気づけないJSON .stringify ({name : "taro" });
tstypeUser = {id : number;name : string };// idプロパティが足りないことに気づける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'.JSON .stringify ({name : "taro" }satisfies User );// satisfiesがないと、idプロパティが足りないことに気づけないJSON .stringify ({name : "taro" });
もしsatisfiesを使わないとなると、型チェックのために一度変数に入れて型注釈を書く必要があり、コードの形も変えなければなりません。
tstypeUser = {id : number;name : string };constuser :User = {id : 1,name : "taro" };constjson =JSON .stringify (user );
tstypeUser = {id : number;name : string };constuser :User = {id : 1,name : "taro" };constjson =JSON .stringify (user );
似た例として、default exportの値をチェックしたい場合にも便利です。
tstypeConfig = {textSize : "small" | "medium" | "large" | number };// "extra-large"が設定ミスであることに気づけるexport default {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" } satisfies textSize Config ;
tstypeConfig = {textSize : "small" | "medium" | "large" | number };// "extra-large"が設定ミスであることに気づけるexport default {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" } satisfies textSize Config ;
もしsatisfiesを使わないとなると、型チェックのために一度変数に入れる必要があります。
tstypeConfig = {textSize : "small" | "medium" | "large" | number };constType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config :Config = {: "extra-large" }; textSize export defaultconfig ;
tstypeConfig = {textSize : "small" | "medium" | "large" | number };constType '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.2322Type '"extra-large"' is not assignable to type 'number | "small" | "medium" | "large"'.config :Config = {: "extra-large" }; textSize export defaultconfig ;
constアサーションとの組み合わせ
constアサーション(as const)は、値を変更不可(readonly)かつリテラル型として扱うようTypeScriptコンパイラに指示する機能です。これとsatisfiesを組み合わせると、値を厳格なリテラル型として固定しつつ、型チェックも行うことができます。
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};// constアサーションを使わない場合constconfig1 = {textSize : 10 } satisfiesConfig ;// constアサーションを使った場合constconfig2 = {textSize : 10 } asconst satisfiesConfig ;
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};// constアサーションを使わない場合constconfig1 = {textSize : 10 } satisfiesConfig ;// constアサーションを使った場合constconfig2 = {textSize : 10 } asconst satisfiesConfig ;
config1は{ textSize: number }と推論されますが、config2は{ readonly textSize: 10 }と推論されます。違いとしてtextSizeがnumber型よりも具体的な10(リテラル型)になっていることがわかります。
ソースコードには10と書いてあるので、よりコード通りの情報を型に持たせられていると言えます。number型よりも具体的な情報が保持されることで、型情報を用いた発展的な処理につなげていくことができます。
少し応用的な例になりますが、テンプレートリテラル型と組み合わせた例を見てみます。configA(as constなし)は単なるnumberなので推論結果が"${number}px"になりますが、configB(as constあり)は10という値を持っているので"${10}px"(つまり"10px")という厳密な型を導き出せます。
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};constconfigA = {textSize : 10 } satisfiesConfig ;constconfigB = {textSize : 10 } asconst satisfiesConfig ;// Config.textSizeの数値から"[数値]px"というリテラル型を導出する型レベル関数typePx <T extendsConfig > = `${T ["textSize"]}px`;// constアサーションなしのほうは [数字]px という文字列が代入可能constfontSize1 :Px <typeofconfigA > = "10px"; // OKconstfontSize2 :Px <typeofconfigA > = "999px"; // OK// constアサーションありのほうは "10px" という文字列だけに限定できるconstfontSize3 :Px <typeofconfigB > = "10px"; // OKconstType '"999px"' is not assignable to type '"10px"'.2322Type '"999px"' is not assignable to type '"10px"'.: fontSize4 Px <typeofconfigB > = "999px"; // エラー
tstypeConfig = {textSize : "small" | "medium" | "large" | number;};constconfigA = {textSize : 10 } satisfiesConfig ;constconfigB = {textSize : 10 } asconst satisfiesConfig ;// Config.textSizeの数値から"[数値]px"というリテラル型を導出する型レベル関数typePx <T extendsConfig > = `${T ["textSize"]}px`;// constアサーションなしのほうは [数字]px という文字列が代入可能constfontSize1 :Px <typeofconfigA > = "10px"; // OKconstfontSize2 :Px <typeofconfigA > = "999px"; // OK// constアサーションありのほうは "10px" という文字列だけに限定できるconstfontSize3 :Px <typeofconfigB > = "10px"; // OKconstType '"999px"' is not assignable to type '"10px"'.2322Type '"999px"' is not assignable to type '"10px"'.: fontSize4 Px <typeofconfigB > = "999px"; // エラー
このようにas constとsatisfiesの合わせ技は、より厳密な型定義が必要な場面で強力な味方になってくれるでしょう。