📘

TypeScript4.9で追加されたsatisfiesを理解する

2022/12/22に公開

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator

TypeScript4.9で追加されたsatisfies演算子がなかなかに使えそうだと言うことで、理解するために色々まとめてみました

そもそも

なぜsatisfies演算子が必要になったのかと言うと、、、

TypeScript developers are often faced with a dilemma: we want to ensure that some expression matches some type, but also want to keep the most specific type of that expression for inference purposes.

TypeScriptの開発者はしばしばジレンマに直面する。ある式がある型に必ずマッチするようにしたいが、推論のためにその式の最も具体的な型も残しておきたいのである。

具体的に何をするのか

これだけ見ても分からないので、詳しくコードを書いて確かめてみましょう
TypeScriptのドキュメントの例に倣って色を管理するオブジェクトを作ってみましょう

type RGBA = {
  r: number;
  g: number;
  b: number;
  a: number;
};

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255, 1],
  white: {
    r: 255,
    g: 255,
    b: 255,
    a: 1,
  },
};

TypeScriptの例からちょっとアレンジしました
このようにredgreenbluewhiteをキーにとるパレットオブジェクトを作成しました
これだとなんの型の強制力もないので、もしキーを間違って

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  bleu: [0, 0, 255, 1], // blueをbleuとタイポしてしまった
  white: {
    r: 255,
    g: 255,
    b: 255,
    a: 1,
  },
};

としてもなんのエラーも出ません
これだとこの定数を使用するプログラマの人が困るので、ミスを避けるために何かしらの型の強制力をつけたいですよね

といってもどう型をつければいいでしょうか?

単純に型を作成する

keyは分かっているが、valueは文字列から数字のタプル(これも3個のものから4個のものもあります)、さらにオブジェクトもvalueとしてとり得る可能性があります

愚直に

type Palette = {
  red: [number, number, number];
  green: string;
  blue: [number, number, number, number];
  white: RGBA;
}

としてもいいですが、もしredに透明値が必要になり、HEXやRGBAにしたくなったら大変です
また、今はキーが4つですがこれが増えたらその都度Palette型も変更しなくてはいけません

RecordとUnionの併用

じゃあ次はちょっとスマートにしてみましょう

type Colors = "red" | "green" | "blue" | "white";
type Palette = Record<
  Colors,
  [number, number, number] | string | [number, number, number, number] | RGBA
>;

キーをUnionとして型で定義し、Recordとしてまとめました
これなら新しい色をPaletteに追加しようとしてもColorsに新しいキーを追加すればOKですし
新しい色指定の型を追加しようとしてもRecordの第二GenericsのUnionに新しい型を追加すれば済みます

ですが、これはこれで問題があります
それは使用時に型がUnionになるという問題です
例えば、redのvalueを使用したい時、上のような型を使用すると以下のような感じになります

const palette: Palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255, 1],
  white: {
    r: 255,
    g: 255,
    b: 255,
    a: 1,
  },
};

palette.red // string | [number, number, number] | [number, number, number, number] | RGBA

palette.redの型がUnionのまま定まらず、使用するごとに対象の型へキャストしなければいけません
これだと型の安全性などあってないようなものです
もしredを間違ってRGBA型に変換しようものなら事故ります

satisfies

ここでsatisfies演算子の出番です
今回の例のように、オブジェクトをある型に一致させるように強制はしたいが、その値に関しては型推論をしてよしなにして欲しい場合にsatisfies演算子は効果を発揮します

では早速使ってみましょう

type Colors = "red" | "green" | "blue" | "white";
type Palette = Record<
  Colors,
  [number, number, number] | string | [number, number, number, number] | RGBA
>;

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255, 1],
  white: {
    r: 255,
    g: 255,
    b: 255,
    a: 1,
  },
} satisfies Palette;

こんな感じで、定数の後ろにsatisfies <型>をつけることで使用できます
使ってみた結果が以下のような感じ

ちゃんと型推論された結果がvalueの型として適用されているのがわかりますね

これをもし間違ってbluebleuにしようものなら

このようなエラーが

間違ってRGBAのaの部分を文字列にしても

のようなエラーできちんとプログラマに知らせてくれます

これで、使用時にもストレスなくかつ、型のことを考慮せず値を使用できるようになりました

まとめ

satisfiesは先の例題のようにある程度曖昧な型を強制させつつ、かつ型推論の結果も使用するという痒い所に手が届いたものになっているかなと思います

型注釈ではvalueの型がUnionになって面倒だった場面が割とあったので今回のこのsatisfies演算子の追加は個人的にかなり嬉しい変更となりました

Discussion