🎄

satisfies / TypeScript一人カレンダー

2024/12/20に公開

こんにちは、クレスウェア株式会社の奥野賢太郎 (@okunokentaro) です。本記事はTypeScript 一人 Advent Calendar 2024の12日目です。昨日は『noUncheckedIndexedAccess / TypeScript一人カレンダー』を紹介しました。

satisfiesで型適合性を柔軟に示す

TypeScript 4.9で追加されたsatisfies演算子は、オブジェクトリテラルや配列リテラルなどの変数が特定の型を「満たしている」ことをコンパイラに示すための新しい仕組みです。2年前のカレンダーでは、まだ筆者自身がこの機能を使いこなせていなかったため紹介を見送りましたが、今では筆者のなかで実務的な活用例が蓄積してきましたので紹介に至りました。

satisfiesは難しいと感じる、あるいは存在自体を知らないという開発者もまだ見かける印象があります。しかし、実際に使ってみると非常に便利な機能で、TypeScriptコードをより厳密に書けるようになります。本記事ではsatisfiesが無かった時代のコードと比較しながら、その利点を明らかにします。

変数型アノテーションとas constの組み合わせによる歪み

まずはsatisfiesがない時代を振り返ってみましょう。以下は、Item型を定義し、オブジェクトリテラルを変数宣言し、その変数のプロパティを書き換える例です。

type Item = {
  id: string;
  name: string;
};

// 変数型アノテーションなし、as constなし
const item1 = { id: "id1", name: "name1" };

// 変数型アノテーションあり、as constなし
const item2: Item = { id: "id2", name: "name2" };

// 変数型アノテーションなし、as constあり
const item3 = { id: "id3", name: "name3" } as const;

// 変数型アノテーションあり、as constあり
const item4: Item = { id: "id4", name: "name4" } as const;

item1.id = "rewrite";
item2.id = "rewrite";
item3.id = "rewrite";
item4.id = "rewrite";

as constを付けた場合、オブジェクトはリテラル型かつreadonlyなオブジェクトとなり、item3.idの書き換えがエラーになることがわかります。しかし、item4のように変数型アノテーション: Itemas constを併用すると、意図に反してidを書き換え可能な状況が生まれます。as constで得たreadonlyの特性が、: Itemによって打ち消されてしまっているのです。

また、次のサンプルコードでは、Item型にprice: numberを追加しています。そこでエラーがどのように表示されるか、Playground上で見てみましょう。

すると、item2item4では変数宣言時点でエラーが出ています。これは、この二つには変数型アノテーションが指定されているからで、priceが足りていないことを検証します。一方でitem1item3は、Itemに合致しなくてもエラーになっていません。これは「Item型に違反しているのか、たまたま今までがItem型と一致していただけなのか」を推論しようがないからです。

ここからわかることとして、as constのみでは型の決定を制御できないケースがあるということです。より具体的にいうと、このケースではas constだけを使ってしまうとpriceの追加漏れに気付くのが遅くなるということです。

satisfiesで型を表現

ここで使用するのがsatisfiesです。

const item5 = { id: "id5", name: "name5" } satisfies Item;
const item6 = { id: "id6", name: "name6" } as const satisfies Item;

satisfiesを使うと、オブジェクトが指定した型を満たしているかコンパイラが検証しますが、変数自体をその型に固定せず、推論結果を優先します。

そのおかげで、as constを併用することで、型要件を満たしたうえでリテラル型情報やreadonly特性を保つことができます。as const satisfies Itemと書けば、Itemを満たしているか厳密に検証しつつ、as constによるリテラル型やreadonly特性を損なわずに保持できます。これによって、型注釈だけの時代やas constのみの時代にはやりづらかった「型チェックをしっかり行いながら、リテラル型の推論も維持する」というバランスが実現します。

続いてItem型にpriceプロパティを足したときに、どのように開発者が漏れに気付けるかというエラーの出方も確認しましょう。

このように、item5変数とitem6変数では、satisfies自体にエラーの赤下線が引かれており、priceプロパティが漏れていることがわかります。

satisfiesは難しそう?いや文字数が長いだけ

satisfiesという単語、letconstclassreturnに比べると明らかに文字数が多く、なんだか難しそうですか?正直、字面が長いというだけで「よくわからない機能」って思ってしまうのも無理はありません。でも、実態は文字数がほかより比較的長いだけで、機能的にはとても便利であり、身構えるものではありません。

as const satisfies Tというパターンは、satisfiesの登場から2年が経ち筆者は使わない日がないようになりました。とても高頻度で使っています。そしてsatisfiesを使うようになってから、逆に変数型アノテーションでオブジェクトの型を記述する場面は激減しました。as TはコンパイラにT型であると信じさせるアサーションなので使い方によっては欺くことが可能で、慎重に扱う必要があります。satisfies Tはそうではなく型合致性を厳密にチェックするため、不正な値を紛れ込ませるリスクを無くすことができます。

satisfiesは難しそうという意見を聞いたことがありますが、それはこれまでの変数型アノテーションやas constといった指定との違いを明確に理解していない場合が多い印象です。「なんとなく難しそう」ではありません。違いが分かれば、satisfiesが上級者向けテクニックではなく、日常的に使うことで型安全性とコードの整合性を保ちやすい初歩的な機能であると気づくでしょう。

筆者は特にTypeScriptビギナーにもsatisfiesを積極的に使って欲しいと考えます。これを使えば、エディタ上の赤下線やコンパイルエラーが「不正な値」や「不十分なプロパティ定義」を即座に指摘してくれるため、ミスの発見が早まり、より安全なコードを書く習慣が身につきます。実務で使ってこそ価値があるsatisfiesを、ぜひ今日から積極的に使ってみてください。

明日は『実例 mustFind()』

本日は「satisfies」を紹介しました。明日は「実例 mustFind()」を紹介します。それではまた。

Discussion