もうsatisfiesのないTypeScriptには戻れない

2024-03-19
2024-03-27

TypeScript 4.9satisfies演算子が追加されて以降、もうこれなしのTypeScriptなんては考えられなくなってしまいました。様々な場面で便利なので、satisfies演算子の使い道についてまとめてみます。

基本の使い方

satisfies演算子は型推論の結果を失わずに型をチェックするために実装されました。

どういうことかというと、例えば、

const colors = [
{
id: 1,
name: 'red',
},
{
id: 2,
name: 'blue',
},
] as const;
type ColorName = (typeof colors)[number]['name'];
// => "red" | "blue"

こんな感じで配列からリテラル型のユニオンとしてColorName型を得たいとします。しかし、これだとColorName型は正しく得られるものの、以下のようなtypoに気づくことができません。

const colors = [
{
id: 1,
name: 'red',
},
{
ie: 2, // typo
name: 'blue',
},
] as const;
type ColorName = (typeof colors)[number]['name'];
// => "red" | "blue"

なので、colors変数が正しいことを保証するために型注釈を追加してみますが、

type Color = {
id: number;
name: string;
};
const colors: Color[] = [
const colors = [
{
id: 1,
name: 'red',
},
{
id: 2,
name: 'blue',
},
] as const;
type ColorName = (typeof colors)[number]['name'];
// => string

これだとcolors変数は正しいものの、型注釈によってwideningが発生し、ColorNamestring型になってしまいます。

そこで、

type Color = {
id: number;
name: string;
};
const colors = [
{
id: 1,
name: 'red',
},
{
id: 2,
name: 'blue',
},
] as const satisfies Color[];
type ColorName = (typeof colors)[number]['name'];
// => "red" | "blue"

こんな感じでsatisfies演算子を使ってあげることで、型推論の結果を活かしながらcolors変数の型をチェックできます。もちろん、typoも検出できます。

type Color = {
id: number;
name: string;
};
const colors = [
{
id: 1,
name: 'red',
},
{
ie: 2, // Error: Object literal may only specify known properties, and 'ie' does not exist in type 'Color'.
name: 'blue',
},
] as const satisfies Color[];
type ColorName = (typeof colors)[number]['name'];
// => "red" | "blue"

より現実的な例としては、AstroでgetStaticPaths()の型チェックのためにsatisfies演算子が活用されていたりします。

別の使い方

型注釈は代入する時のみ利用できるのに対し、satisfies演算子は代入を伴わずに式が返す値の型をチェックできます。

この特徴を利用して以下のような使い方も考えられます。

switch文の網羅性チェック(exhaustiveness check)をする

こんなコードがあったとします。

type Subject =
| { type: 'Math'; level: 'Basic' }
| { type: 'Science'; level: 'Intermediate' }
| { type: 'History'; level: 'Advanced' };
const subject: Subject = { type: 'History', level: 'Advanced' };
const printLevel = (subject: Subject) => {
switch (subject.type) {
case 'Math': {
console.log(`Math: ${subject.level}`);
break;
}
case 'Science': {
console.log(`Science: ${subject.level}`);
break;
}
}
}
printLevel(subject);

subject.type'History'のときの処理を書き忘れているため、このコードを実行してもなにも表示されません。

しかし、以下のようにして型レベルで網羅性を保証してあげることができます。

const printLevel = (subject: Subject) => {
switch (subject.type) {
case 'Math': {
console.log(`Math: ${subject.level}`);
break;
}
case 'Science': {
console.log(`Science: ${subject.level}`);
break;
}
default:
subject satisfies never; // Error: Type '{ type: "History"; level: "Advanced"; }' does not satisfy the expected type 'never'.
}
}

もっとも、satisfies演算子を使わずとも、デフォルトケースで

const _assert: never = subject;

のようにしたり、never型の引数を取る関数にsubjectを渡すことで網羅性チェックはできます。

しかし、前者の方法では_assert is declared but its value is never read.のエラーが出てしまい、後者の方法ではわざわざアサート用の関数を用意しなければいけないという欠点があります。

それを考慮するとやはりsatisfiesを使って網羅性チェックをするのがキレイかなーと思っています。

最後に

改めてまとめてみたらあんまり使い道多くなかったですね。TypeScriptを書いているときは事あるごとにsatisfiesってタイプしてはsatisfiesが使える現代に生まれたことを感謝していたつもりだったのですが、ただの気のせいだったようです。

もし他にも便利な用途を見つけたら追記するかもしれません。

© 2024 tsukiyo