12k
All articles

TypeScript の `satisfies` 演算子 実践ガイド

TypeScriptのsatisfies演算子を設定例で解説。狭い型推論を保ち、asや型注釈との使い分けもわかります。

OpenReplay Team
OpenReplay Team
TypeScript の `satisfies` 演算子 実践ガイド

satisfies 演算子は、値の推論型を変えることなく、値を型に対して検証します。つまり、型の安全性と値の狭い型(narrow type)を同時に得ることができます。この単一の動作が、日常的なある特定の問題を解決します。コロンによるアノテーションでコンフィグオブジェクトを型付けすると、誤った値からは守られますが、保持したかったリテラルキーや狭い型が失われてしまいます。本ガイドでは、メンタルモデル、典型的な使用例、そして satisfiesas やコロンアノテーションの代わりに選ぶための判断基準を解説します。すべてのコードスニペットは TypeScript 4.9 以降を対象としています。

重要なポイント

  • satisfies は値の狭い推論型を保持しながら型に対して検証を行うため、オートコンプリートとリテラルの型の絞り込みが維持されます。
  • コロンアノテーションでは宣言された型が優先され、値はその型に合わせて拡張(widen)されます。satisfies では値が優先され、型は検証にのみ使用されます。
  • as は値を検証しません。型チェッカーを上書きするだけです。そのため const user = {} as User はコンパイルが通りますが、user.name を参照した瞬間に実行時エラーが発生します。
  • コロンアノテーションと satisfies を同時に使う(const x: T = {…} satisfies T)のは冗長です。コロンが優先され、satisfies で得たかった型の絞り込みが無効になります。
  • satisfies はコンパイル時のみ機能し、JavaScript を一切出力しません。ネットワークやファイルからデータを受け取る場合は、Zod などのランタイムバリデーターを使用してください。

satisfies が解決する問題

satisfies を使いたくなる場面は主に 2 つあります。1 つ目は、キーを持つオブジェクトへのコロンアノテーションです。これにより型が拡張され、オートコンプリートが機能しなくなります。Matt Pocock の routes の例がその典型です。Record<string, {}> でアノテーションすると、存在しないキーを参照してもエラーになりません。

const routes: Record<string, {}> = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};
routes.awdkjanwdkjn; // エラーなし — 型が Record<string, {}> になっているため

2 つ目は、ユニオン型のプロパティです。freeCodeCamp の例では、文字列リテラルとオブジェクトのユニオン型として型付けされたプロパティに対して、手動のガード処理なしに文字列メソッドを呼び出すことができません。

type Info = "John" | "Jack" | { id: number; age: number };
type Person = { myInfo: Info; myOtherInfo: Info };

const applicant: Person = { myInfo: "John", myOtherInfo: { id: 123, age: 22 } };
applicant.myInfo.toUpperCase();
// Property 'toUpperCase' does not exist on type 'Info'

アクセスのたびに if (typeof applicant.myInfo === "string") を書く羽目になります。どちらの問題も根本原因は同じです。コロンアノテーションによって、具体的な値の型がより広い宣言型に置き換えられてしまうのです。

satisfies 演算子とは何か

satisfies は、TypeScript が推論する型を変えることなく、式が型に一致するかを検証します。TypeScript 4.9 で導入され、2022 年 11 月 15 日にリリースされました。現在の TypeScript 6.0 安定版および 7.0 リリース候補でも同様に動作します。7.0 の Go 移植版は型チェックのセマンティクスを 6.0 と構造的に同一に保っているため、satisfies は新しいコンパイラでも全く同じルールを適用します。

Pocock の表現を借りたメンタルモデルは次の通りです。コロンアノテーションでは型が値に勝ち、satisfies では値が型に勝ちます。 satisfies を使うと、TypeScript は可能な限り狭い型を推論し、アノテーションは検証にのみ使用します。コンパイル時のみ機能し、JavaScript を一切出力しないため実行時コストはゼロです。コードが実行される前に、タイポや誤った値の型を検出できます。

satisfies の典型的な使用例

解決策は、アノテーションをコロンから末尾の satisfies に移すことです。これにより両方の問題が一度に解消されます。狭いリテラル型を保持しながら、誤った値に対してエラーを得ることができます。

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
} satisfies Record<string, {}>;

routes.awdkjanwdkjn;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'

routes のオートコンプリートには実際のパスが表示されます。検証も機能します。アノテーションが禁止する値を代入しようとすると、コンパイラが拒否します。

const routes = {
  "/": null, // Type 'null' is not assignable to type '{}'
} satisfies Record<string, {}>;

同じ手法でユニオン型の問題も解決できます。applicant.myInfo はリテラル "John" に絞り込まれるため、ガード処理なしに .toUpperCase() を呼び出せます。

const applicant = {
  myInfo: "John",
  myOtherInfo: { id: 123, age: 22 },
} satisfies Person;

applicant.myInfo.toUpperCase(); // OK — "John" として推論される

satisfies vs as vs コロンアノテーション

as は値を検証しません。型チェッカーを上書きするだけです。そのため const user = {} as User はコンパイルが通りますが、user.name を参照した瞬間に実行時エラーが発生します。これが 3 つのツールの核心的な違いです。

ツール値を検証するか狭い推論を保持するかTypeScript に嘘をつけるか使用場面
: Type(コロン)はいいいえ — 型に合わせて拡張されるいいえ意図的に広い型が必要な場合
satisfies Typeはいはいいいえ検証と狭い推論の両方が必要な場合
as Typeいいえ該当なしはいほぼ使用すべきでない

実行時の危険性は具体的です。

type User = { id: string; name: { first: string; last: string } };
const user = {} as User;
user.name.first; // IDE ではエラーなし — 実行時にエラーが発生する

as は静かに腐敗していきます。現時点ではコンパイルが通りますが、User に必須フィールドを追加すると defaultUser が無効になってもエラーが出ません

type User = { id: string; name: string };
const defaultUser = { id: "123", name: "Matt" } as User;

これはセッションリプレイが表面化するために設計されたバグの典型例です。プロパティアクセスでエラーが発生した瞬間の実際のオブジェクトの形状が確認でき、アサートした形状ではなく実際の状態がわかります。satisfies に置き換えれば、コンパイラが不足しているフィールドを即座に検出します。

基本原則: デフォルトで as を使うのは避けましょう。推論を保持しながら検証したい場合は satisfies を使い、後で再代入するために意図的に広い型が必要な場合のみコロンアノテーションを使ってください。

冗長なアノテーションの罠

コロンアノテーションと satisfies を同時に使う(const joe: TUser = {…} satisfies TUser)のは冗長です。コロンアノテーションが優先され、satisfies で保持しようとした型の絞り込みが暗黙的に無効になります。Refine.dev の解説では、宣言された型が優先されて内部の絞り込みが失われるため、ネストされたプロパティへのアクセスが失敗する結果を示しています。どちらか一方を選んでください。型の絞り込みが必要なら、コロンを削除してください。

satisfies が真価を発揮する場面

型付きコンフィグ、Record キーのマップ、絞り込みを維持したい判別ユニオン(discriminated union)の値に satisfies を使いましょう。テーマやパレットのマップが典型例です。公式の palette の例では、各 RGB エントリを検証しながら、キーごとのリテラル型を保持しています。オプショナルなキーには Partial でレコードをラップすることで、存在しないキーを許容しつつ、存在するキーは引き続き検証されます。

type Keys = "id" | "name" | "email" | "age";

const person = {
  id: 12345,
  name: "Jacky",
  email: "jacky@test.com",
} satisfies Partial<Record<Keys, string | number>>;

person.name.toUpperCase(); // string に絞り込まれる

satisfies を使わない方がよい場面

シンプルなオブジェクトで : Type アノテーションで十分な場合は satisfies を使う必要はありません。また、意図的に広い型が必要な場合も同様です。後で変数を再代入する予定がある場合、satisfies は狭い推論型に固定してしまうためブロックされます。

// コロンアノテーション — 再代入が可能
let id: string | number = "123";
id = 456; // OK

// satisfies — 値が優先されるため string に絞り込まれる
let id2 = "123" satisfies string | number;
id2 = 456; // Type 'number' is not assignable to type 'string'

また、自分でコントロールできないデータには satisfies を使わないでください。satisfies は実行時に動作しないため、JSON ペイロードやフォームの入力値を検証することはできません。データがネットワークやファイルの境界を越える場合は、Zod や io-ts などのランタイムスキーマバリデーターを使用してください。

リテラルを型付けしつつ具体的な型を保持したい場合(コンフィグ、ルートマップ、パレット、ユニオン値)には satisfies を積極的に活用しましょう。反射的に使っていた assatisfies に置き換え、コロンアノテーションは意図的に広い型が必要な場合に限定することで、次に書くコンフィグオブジェクトは安全性とオートコンプリートの両方を兼ね備えたものになります。

よくある質問

satisfies 演算子は JSDoc を使った JavaScript ファイルでも動作しますか?

はい。TypeScript 5.0 では @satisfies JSDoc タグが追加されており、TypeScript ファイルの satisfies 演算子と同じ動作をします。JavaScript ファイルでは宣言の上にタグを記述します。例えば型名を指定した @satisfies アノテーションを書くと、チェッカーがその型に対して値を検証しながら、狭い推論型を保持します。これにより、.ts ファイルに移行することなく、JSDoc で型付けされた JavaScript プロジェクトでも同様の検証と型の絞り込みの恩恵を受けることができます。

satisfies は実行時コストを追加したり、余分な JavaScript を生成したりしますか?

いいえ。satisfies はコンパイル時のみ機能する型レベルの演算子であり、JavaScript を一切出力しません。そのため実行時コストもバンドルサイズへの影響もありません。キーワードとその後の型はコンパイル時に消去されます。これはコロンアノテーションと全く同じです。実行時には何も動作しないため、ネットワークペイロードやフォーム入力のデータ検証には Zod などのランタイムスキーマバリデーターが引き続き必要です。

コロンアノテーションと satisfies を同時に使っているのに型チェックが失敗するのはなぜですか?

コロンアノテーションが常に優先され、satisfies 句が無効な構文になるためです。const config: Theme = {…} satisfies Theme と書くと、宣言された Theme 型が優先されて値が Theme に拡張され、satisfies で保持しようとした狭い推論が破棄されます。その結果、ネストされたリテラルプロパティへのアクセスは satisfies がないのと同様に失敗します。コロンを削除して末尾の satisfies のみを残すことで、型の絞り込みを維持できます。

データの検証に satisfies の代わりに Zod を使うべき場合はいつですか?

ネットワークレスポンス、JSON ファイル、フォームの送信など、自分でコントロールできないソースから実行時にデータが届く場合は、Zod や io-ts などのランタイムスキーマバリデーターを使用してください。satisfies はソースコードに記述したリテラルのみをコンパイル時にチェックし、実行時コードを出力しないため、外部からの未知のデータを検査することはできません。自分のコード内の型付きコンフィグ、Record マップ、ユニオン値には satisfies を使い、外部の境界には Zod を使いましょう。

We use cookies to improve your experience. By using our site, you accept cookies.