[TypeScript] satisfies のつかいかた
TypeScript 4.9 で導入された satisfies
演算子の使い方を紹介しています。
- 0. satisfies について
- 1. より安全に型を与える (Safe upcast)
- 2. オブジェクトリテラルの型をチェックする (一時変数が不要に)
- 3. オブジェクトの型を広げずに型チェックする (as const と組み合わせるなど)
- 4. 型ガードによる分岐などで念のため型チェックする
- まとめ
0. satisfies について
satisfies
演算子は、「型を変えることなく型チェックを行う」演算子です。その構文(SatisfiesExpression)は以下の通りです。(参考: TS 4.9.4 のソースコード)
expression satisfies type
「expression」に値を計算する式など、「type」に型(型参照)を指定します。「expression」に指定する式はなんでも構わないため、定数(定数オブジェクトなど)だけでなく関数の戻り値や変数も指定することができます。また、type に指定できる型は「as
」で用いることができる型と同じものが使用できます(型名や共用体型はもちろん、typeof
による型クエリーも使うことができます)。
似たようなものとして「as
」がありますが、as
はあくまでも「指定の型とみなす」ものであって厳密な型チェックはしないため、satisfies
を使うことでより安全に型・データを扱うことが可能になります。
1. より安全に型を与える (Safe upcast)
satisfies
を使うことで、as
を使った場合でも安全に型を与えることができます。例えば以下のコードがあるとします。
interface User {
name: string;
location: string;
}
interface Group {
name: string;
users: User[];
}
type UserOrGroup = User | Group;
このうち User
のデータを持つオブジェクトを以下のように作るとします。
const obj = {
name: 'Taro',
location: 'Tokyo'
};
この obj
を UserOrGroup
型として扱いたい場合、従来は
const taroUser = {
name: 'Taro',
location: 'Tokyo'
} as UserOrGroup;
などといった書き方をしていた方もいると思います。しかし「as
」は「データが指定の型の一部分に当てはまる場合その型とみなす(アップキャスト)」ものであるので、もし User
の型が
interface User {
name: string;
location: string;
age: number | null; // 追加
}
と変更されたとしても、taroUser
の部分で型チェックエラーが発生せず、結果ランタイムエラーが起きてしまう可能性があります。
そこで satisfies
を以下のように使うことで、
const taroUser = {
name: 'Taro',
location: 'Tokyo'
// ↓ satisfies の部分でエラーを検知してくれる
} satisfies UserOrGroup as UserOrGroup;
オブジェクトの型をチェックしながらアップキャストすることができます。
※ この例では、「const taroUser: UserOrGroup = { ... }
」とする方法もあります。
2. オブジェクトリテラルの型をチェックする (一時変数が不要に)
satisfies
は「式」をチェックするものなので、代入文や関数の引数として与える式にも型チェックとして satisfies
を使うことができます。
以下の例のように、オブジェクトを送信したい場合があるとします。
window.parent.postMessage({
type: 'text',
value: 'hello'
});
ここで与えているオブジェクトが期待した型に合致しているかどうかをチェックする場合、従来は
const msg: TextMessage = {
type: 'text',
value: 'hello'
};
window.parent.postMessage(msg);
のように一時変数を与えてチェックするぐらいしか方法がありませんでした。こんなとき、satisfies
を使うことで
window.parent.postMessage({
type: 'text',
value: 'hello'
} satisfies TextMessage);
と書くことができ、一時変数を使わずにオブジェクトリテラルの型をチェックすることができます。
ちなみに、satisfies
を使うことでオブジェクトリテラルの型が狭まることがあります。
interface TextMessage {
type: 'text';
value: string;
}
const msg1 = {
type: 'text',
value: 'hello'
}; // msg1 は { type: string, value: string }
const msg2 = {
type: 'text',
value: 'world'
} satisfies TextMessage;
// msg2 は { type: 'text', value: string }
satisfies
を使わない msg1
については「type
」は string
型になりますが、satisfies
を使った msg2
については「type
」は 'text'
型になります。
3. オブジェクトの型を広げずに型チェックする (as const と組み合わせるなど)
オブジェクトの型をチェックしたいけどオブジェクト自体の型を変えたくない(広げたくない)というようなケースにも satisfies
が適しています。具体的には、
interface TheData {
a: 'A' | 'B' | 'C';
b: number;
c: string;
d: string[];
}
// 「TheData」の一部を定義
const baseData = {
a: 'A',
b: '12' // 誤って文字列を指定
};
// baseData を使いまわしてデータを作るがエラーになる
const data1: TheData = {
...baseData,
c: 'foo',
d: []
};
と、baseData
で誤りがあったときにそれを早めに検出したいと考えます。こんなとき、baseData
は「TheData
」の一部のみを定義しているので、Partial
を使って
// 「TheData」の一部を定義
const baseData: Partial<TheData> = {
a: 'A',
b: '12' // 誤って文字列を指定しているのでエラーになる
};
と型を与えてやれば、b
の誤りを検出することはできます。しかし、上記のように書くと今度は baseData
が型「Partial<TheData>
」となって、
// baseData を使いまわしてデータを作るがまだエラーになる
const data1: TheData = {
...baseData,
c: 'foo',
d: []
};
の定義で baseData
の型が合わなくなってエラーになります。
そこで、型を与える代わりに satisfies
を用いて
// 「TheData」の一部を定義しているが、型は { a: 'A', b: string } になる
const baseData = {
a: 'A',
b: '12' // 誤って文字列を指定しているのでエラーになる
} satisfies Partial<TheData>;
とすれば、baseData
の型を変えず(広げず)に誤りを検出することができます。
もう一つの例として、以下のように
const names = ['Akari', 'Sumire', 'Hinaki'];
type Names = (typeof names)[number]; // string
と書いて names
の各エントリーの共用体(union)型を得ようとします。このコードだと string
となってしまうため、「as const
」を使って
// 名前の配列を定義
const names = ['Akari', 'Sumire', 'Hinaki'] as const;
type Names = typeof names[number]; // 'Akari' | 'Sumire' | 'Hinaki'
と書けば共用体型を得ることができます。ここで、エントリーを増やそうとして誤って
// 名前の配列を定義
const names = ['Akari', 'Sumire', 'Hinaki',, 'Juri'] as const;
type Names = typeof names[number]; // 'Akari' | 'Sumire' | 'Hinaki' | 'Juri' | undefined
のように「,,
」とタイプミスしてしまうと、エラーは出ずに Names
に「undefined
」型が混ざってしまうことになります。それを防ぐのに使えるのが satisfies
で、
// 名前の配列を定義したものの型チェックエラーになる
const names = ['Akari', 'Sumire', 'Hinaki',, 'Juri'] as const satisfies readonly string[];
と書けばエラーを検出することができます。
4. 型ガードによる分岐などで念のため型チェックする
少し変わった使い方として、型の絞り込み(type narrowing)の結果が意図したものになっているかどうかを確認する目的で satisfies
を使うことができます。例えば以下のような分岐があるとします。
interface Foo {
type: 'foo';
a: string;
}
interface Bar {
type: 'bar';
b: string;
}
type SomeObject = Foo | Bar;
function test(o: SomeObject) {
if (o.type === 'foo') {
// o は Foo
} else if (o.type === 'bar') {
// o は Bar
} else {
// ここには来ないはず…
}
}
test
関数における if 文では、「type
」フィールドをチェックすることで型ガードをすることができ、「'foo'
」であれば o
が Foo
であると扱うことができます。同様に次の if 文の中では o
は Bar
として扱えます。
では else 内ではどうなるかというと、SomeObject
が「Foo | Bar
」であれば o
は「never
」型であると推論されます。(そのため、このブロック内では o
をまともに使うことができません。)
ここで、もし SomeObject
の型が変更になって例えば
interface Foo {
type: 'foo';
a: string;
}
interface Bar {
type: 'bar';
b: string;
}
// 追加
interface Baz {
type: 'baz';
c: string;
}
type SomeObject = Foo | Bar | Baz;
function test(o: SomeObject) {
if (o.type === 'foo') {
// o は Foo
} else if (o.type === 'bar') {
// o は Bar
} else {
// o は…?
}
}
と「Foo | Bar | Baz
」となった場合、test
関数内の else ブロックにおける o
の型が変わる(Baz
型になる)ことになります。特にハンドルする必要がなければこれでもよいかもしれませんが、SomeObject
が別ファイルや外部ライブラリなどに定義されている場合、変更に気づけず Bar
型をハンドルし忘れてしまうかもしれません。
そこで、
function test(o: SomeObject) {
if (o.type === 'foo') {
// o は Foo
} else if (o.type === 'bar') {
// o は Bar
} else {
// o (SomeObject) が Foo | Bar であれば以下は問題が出ず、そうでなければ型エラーになる
o satisfies never;
}
}
といった形で satisfies
を用いることで、o
の推論結果(型ガードによる結果)をチェックすることができます。
※ 「オブジェクトの型をより厳密に判定する」にも使用例があります。
まとめ
satisfies
を使うことで、
- 型を安全にアップキャストする際に使えます。
- 一時変数などを使わずにオブジェクトリテラルの型をチェックすることができます。
- オブジェクトの型を広げずに型チェックすることができます。
- データの推論結果をチェックする際に使えます。
(「型を変えずに型チェックを行う」という特性から、この他にも使い道はあるかもしれません。)