satisfies / TypeScript一人カレンダー
こんにちは、クレスウェア株式会社の奥野賢太郎 (@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
のように変数型アノテーション: Item
とas const
を併用すると、意図に反してid
を書き換え可能な状況が生まれます。as const
で得たreadonlyの特性が、: Item
によって打ち消されてしまっているのです。
また、次のサンプルコードでは、Item
型にprice: number
を追加しています。そこでエラーがどのように表示されるか、Playground上で見てみましょう。
すると、item2
やitem4
では変数宣言時点でエラーが出ています。これは、この二つには変数型アノテーションが指定されているからで、price
が足りていないことを検証します。一方でitem1
やitem3
は、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
という単語、let
やconst
、class
、return
に比べると明らかに文字数が多く、なんだか難しそうですか?正直、字面が長いというだけで「よくわからない機能」って思ってしまうのも無理はありません。でも、実態は文字数がほかより比較的長いだけで、機能的にはとても便利であり、身構えるものではありません。
as const satisfies T
というパターンは、satisfies
の登場から2年が経ち筆者は使わない日がないようになりました。とても高頻度で使っています。そしてsatisfies
を使うようになってから、逆に変数型アノテーションでオブジェクトの型を記述する場面は激減しました。as T
はコンパイラにT
型であると信じさせるアサーションなので使い方によっては欺くことが可能で、慎重に扱う必要があります。satisfies T
はそうではなく型合致性を厳密にチェックするため、不正な値を紛れ込ませるリスクを無くすことができます。
satisfies
は難しそうという意見を聞いたことがありますが、それはこれまでの変数型アノテーションやas const
といった指定との違いを明確に理解していない場合が多い印象です。「なんとなく難しそう」ではありません。違いが分かれば、satisfies
が上級者向けテクニックではなく、日常的に使うことで型安全性とコードの整合性を保ちやすい初歩的な機能であると気づくでしょう。
筆者は特にTypeScriptビギナーにもsatisfies
を積極的に使って欲しいと考えます。これを使えば、エディタ上の赤下線やコンパイルエラーが「不正な値」や「不十分なプロパティ定義」を即座に指摘してくれるため、ミスの発見が早まり、より安全なコードを書く習慣が身につきます。実務で使ってこそ価値があるsatisfies
を、ぜひ今日から積極的に使ってみてください。
明日は『実例 mustFind()』
本日は「satisfies
」を紹介しました。明日は「実例 mustFind()
」を紹介します。それではまた。
Discussion