Programming Field

[TypeScript] satisfies のつかいかた

TypeScript 4.9 で導入された satisfies 演算子の使い方を紹介しています。

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'
};

この objUserOrGroup 型として扱いたい場合、従来は

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'」であれば oFoo であると扱うことができます。同様に次の if 文の中では oBar として扱えます。

では 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 を使うことで、

  • 型を安全にアップキャストする際に使えます。
  • 一時変数などを使わずにオブジェクトリテラルの型をチェックすることができます。
  • オブジェクトの型を広げずに型チェックすることができます。
  • データの推論結果をチェックする際に使えます。

(「型を変えずに型チェックを行う」という特性から、この他にも使い道はあるかもしれません。)