TypeScript 4.9のas const satisfiesが便利。型チェックとwidening防止を同時に行う
TypeScript 4.9 から、satisfies
operator が使えるようになりました。従来のas const
と組み合わせ、型チェックと widening 防止を同時に行えます。筆者的には、"顧客が本当に必要だったもの"です。
本記事では satisfies
とは何か? as const
とは何か? 2つを組合わせるとどのようなメリットがあるのか? について、実際のコードと共に解説します。
結論
TypeScriptで 定数を export する場合は、as const satisfies
を設定しておくと便利です。
export const myName = "田中" as const satisfies string;
export const foodList = {
ramen: "ラーメン",
udon: "うどん",
soba: "そば"
} as const satisfies {
[key: string]: string
};
as const satisfies
の挙動
as const satisfies
を使うことで、次の2つのメリットが得られます。
-
satisfies
演算子: 型がマッチするかどうかをチェックできる -
as const
: 値が widening (拡大)しない
個別の機能の説明の前に、まずは as const satisfies
のコードの挙動を確認してみましょう。
▼ my-option.ts
type MyOption = {
foo: string;
bar: number;
baz: {
qux: number;
};
};
export const myOption = {
foo: "foo",
bar: 2,
baz: {
qux: 3,
},
} as const satisfies MyOption;
myOption
オブジェクトが MyOption
型にマッチしない場合、エラーが起こります。
export const myOption = {
// ❌エラー
foo: 120,
bar: 2,
baz: {
// ❌エラー
qux: "HELLO",
},
} as const satisfies MyOption;
myOption
オブジェクトの各プロパティは、「widening」しません(widening については後ほど解説します)。
とくに、 import 先で myOption
オブジェクトを使いたい時、 widening しないことで型が安全になります。
satisfies
とは
satisfies
とは、TypeScript 4.9 で導入された新しい演算子です。次のように定義したとき、colorList
オブジェクトが、 ColorList
型にマッチするかどうかをチェックできます。
type ColorList = {
[key in "red" | "blue" | "green"]: unknown;
};
const colorList = {
red: "#ff0000",
green: [0, 255, 0],
blue: "#0000ff"
} satisfies ColorList;
ColorList
型の [key in "red" | "blue" | "green"]
とは、 オブジェクトのキーが red
か blue
か green
のいずれかという意味です。オブジェクトの値が unknown
なので、次のことを表現した型になります。
- オブジェクトのキーが
red
かblue
かgreen
のいずれか - オブジェクトの値は何でもいい
colorList
オブジェクトが ColorList
型にマッチしない場合、エラーが起こります。
const colorList = {
red: "#ff0000",
green: [0, 255, 0],
// ❌エラー
yellow: "#0000ff"
} satisfies ColorList;
▼ yellow
キーが ColorList
型にマッチしない旨のエラー
satisfies
を使うと、型推論結果が保持される
satisfies
の特徴は、型チェックが行われつつも、型推論結果が保持されることです。 「satisfies」(サティスファイズ)は「(条件などを)満たす、確信させる」といった意味があります。
次の colorList
オブジェクトにおいて、green
は ColorList
の型の一部であることがチェックされ、かつ number[]
型と推論されます。配列なので、配列用メソッド map()
が使えます。
▼ green
は配列なので、配列用のメソッドが使える
const colorList = {
red: "#ff0000",
green: [0, 255, 0],
blue: "#0000ff"
} satisfies ColorList;
// 配列のメソッドを実行
colorList.green.map(value => value * 2);
型注釈では型推論結果が失われる
satisfies ColorList
は、 colorList
オブジェクトに型注釈 : ColorList
をつけることと、何が違うのでしょうか? 動作の違いを確認してみましょう。
const colorList: ColorList = {
red: "#ff0000",
green: [0, 255, 0],
blue: "#0000ff"
};
この場合も colorList
オブジェクトが ColorList
型にマッチしない場合、エラーが起こります。
const colorList: ColorList = {
red: "#ff0000",
green: [0, 255, 0],
// ❌エラー
yellow: "#0000ff"
};
satisfies
と異なるのは推論結果です。型注釈を設定する場合、当然ですが型の推論結果は失われ、colorList
オブジェクトの型情報は ColorList
型になります。
そのため green
プロパティは unknown
となり配列用の関数が使えません。開発者が green
が配列であることを明らかにわかっていても、です。
const colorList: ColorList = {
red: "#ff0000",
green: [0, 255, 0],
blue: "#0000ff"
};
// ❌エラー
// green は unknown なので、 map() は使えない
colorList.green.map(value => value * 2);
補足
type ColorList = { red: string, green: [number, number, number], blue: string };
と表現すれば green
で正しく map
が使えます。しかし、purple
や pink
など、新しくキーを追加するたびにそれに対応する値の型を追加する必要があります。 「オブジェクトの値はなんでもいいので、キーの種類だけを制限したい」という当初の目的からは外れてしまいますし、大して詳細な情報を持たない型注釈と、詳細な情報を持つ値を二重で管理するのも手間です。
「型のチェックをしたいが、推論結果は保持したい」というケースには、型注釈よりも satisfies
が有効です。
as const
とは
satisfies
と一緒に使うと強力なのが as const
です。
as const
とは、次の効果があります。
- 文字列・数値・真偽値などのリテラル型を widening しない
- オブジェクト内のすべてのプロパティが readonly になる
- 配列リテラルの推論結果がタプル型になる
- テンプレート文字列リテラル型の推論結果が widening しない
※ 参考: プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで|技術評論社
widening とは
次の myString
は、リテラル型の「"鈴木"
型」に推論されます。
const myString = "鈴木";
// "鈴木"型
let
で変数を宣言すると、myString2
の型は、リテラル型の "鈴木"
型ではなく、より広い string
になります。なぜなら、 myString2
は別の文字列を割り当て可能なため、"鈴木"
しか入れられない"鈴木"
型は合わないためです。
let myString2 = "鈴木";
// string型
このように、より型が大きくなる挙動のことを widening と呼びます。
オブジェクトの場合、各プロパティが widening し、{ age: number, name: string }
と推論されます。 { age: 18, name: "鈴木" }
とは推論されません。オブジェクトの各プロパティが widening するのは、各プロパティが書き換え可能なことが原因です。
const myObject = {
age: 18,
name: "鈴木",
};
// 推論結果
// {
// age: number,
// name: string
//}
もちろん、配列も widening します。次の例では、 myArray
は string[]
型になります。[string, string, string]
や ["a", "b", "c"]
とは推論されません。
const myArray = ["a", "b", "c"];
// 推論結果はstring[]
import・export での widening
1つのモジュール内での話あれば気をつければ済む話でしょう。問題は、import・export した際です。
他の開発者が myObject
を import してロジックを書いたとします。
export const myObject = {
age: 18,
name: "鈴木",
};
import { myObject } from "./my-object";
// "鈴木"型ではなく、string型
const myName = myObject.name;
myObject.name
は "鈴木"
ではなく、 string
型になってしまいましたが、使用者側は気づきづらいでしょう。
as const
で防ぐ
widening を widening は、as const
で防げます。
オブジェクトにおける widening の抑制例は次のとおりです。
▼ export 側
export const myObject = {
age: 18,
name: "鈴木",
} as const;
▼ import 側
import { myObject } from "./my-object";
// "鈴木"型
const myName = myObject.name;
基本的に readonly な値を export する場合は、as const
をつけておくほうが安全です。筆者のプロジェクトでは、すべてそのようにしています。
satisfies
と as const
を組み合わせる
本記事のキモです。
satisfies
と as const
を組み合わせると、2つのメリットを組み合わせられます。
-
satisfies 型
: 型がマッチするかどうかをチェックできる -
as const
: 値が widening しない
次の MyOption
を使って考えてみましょう。
type MyOption = {
foo: string;
bar: number;
baz: {
qux: number;
};
};
MyOption
型を満たす myOption
オブジェクトを作って、 export したいとします。
export const myOption = {
foo: "foo",
bar: 2,
baz: {
qux: 3,
},
};
satisfies
なし、 as const
なしの場合
myOption
は何一つ型安全ではありません。大規模なプロジェクトになればなるほど、死者が増えるでしょう。
export const myOption = {
foo: "foo",
bar: 2,
baz: {
qux: 3,
},
};
satisfies
あり、 as const
なしの場合
型のチェックはできます。
export const myOption = {
foo: "foo",
// 型エラーになる
bar: "HELLO",
baz: {
qux: 3,
},
} satisfies MyOption;
しかし、 myOption
オブジェクトは widening してしまいます。
▼ myOption
オブジェクトの推論結果
{
foo: string,
bar: number,
baz: {
qux: number,
},
}
他の開発者が myOption
オブジェクトを使う時、型が widening していることに気づきづらいです。
satisfies
なし、 as const
ありの場合
widening は防げますが、型チェックができなくなります。
export const myOption = {
foo: "foo",
// 型エラーにならない
bar: "HELLO",
baz: {
qux: 3,
},
} as const
▼ myOption
の推論結果
{
foo: "foo",
bar: "HELLO",
baz: {
qux: 3,
},
}
satisfies
あり、 as const
ありの場合
型チェックが可能であり、かつ widening も防げます💐
export const myOption = {
foo: "foo",
// 型エラーになる
bar: "HELLO",
baz: {
qux: 3,
},
} as const
▼ myOption
を正しく修正する
export const myOption = {
foo: "foo",
bar: 2,
baz: {
qux: 3,
},
} as const satisfies MyOption;
▼ myOption
の推論結果
{
foo: "foo",
bar: 2,
baz: {
qux: 3,
},
}
▼ IDE での推論結果の確認
各コードのデモは次の URL で確認できます。
as const satisfies
配列と 配列を使う際も、as const satisfies
は便利です。
たとえば、次の myArray
を string[]
で型注釈したとき、typeof myArray[number]
の型(任意のインデックスの要素の型)は string
にしかなりません。
const myArray: readonly string[] = ["りんご", "みかん", "ぶどう"];
type MyElement = typeof myArray[number];
// string型
as const satisfies
を使うと、typeof myArray[number]
の型(任意のインデックスの要素の型)は "りんご" | "みかん" | "ぶどう"
になります。もちろん、myArray
に文字列以外の要素を追加するとエラーになります。
const myArray = ["りんご", "みかん", "ぶどう"] as const satisfies readonly string[];
type MyElement = typeof myArray[number];
// "りんご" | "みかん" | "ぶどう" 型
as const satisfies
プリミティブ型と 文字列・数値といったプリミティブ型の場合にも、as const satisfies
は便利です。
readonly の定数を作りたいが、型は string
や number
に制限しておきたいケースに使えます。前述のように export したプリミティブ型は widening しますので、 as const satisfies
で型を制限しておくと安全です。
// myString は string に制限されつつつ、 'バナナ'型 に推論される
export const myString = 'バナナ' as const satisfies string;
// myNumber は number に制限されつつつ、 18型 に推論される
export const myVersion = 18 as const satisfies number;
// isDevelopMode は boolean に制限されつつつ、 true型 に推論される
export const isDevelopMode = true as const satisfies boolean;
as const satisfies
の活用例
as const satisfies
をどのような場面で活用できるか、いくつか例を挙げます。
▼ アプリケーションのバージョンの定数
export const appVersion = "1.0.2" as const satisfies `${number}.${number}.${number}`;
▼ 元号のリスト
export const eraNames = ["昭和", "平成", "令和"] as const satisfies readonly string[];
▼ URLのリスト
export const urlList = {
apple: "https://www.apple.com/jp/",
google: "https://www.google.com/",
yahoo: "https://www.yahoo.co.jp/",
} as const satisfies {
// 値は https:// で始まるURLに限定する
[key: string]: `https://${string}`
};
▼ ステータスのリスト
export const statusList = [
{
status: "processing",
title: "作業中"
},
{
status: "cancel",
title: "キャンセル"
},
{
status: "completed",
title: "完了"
},
] as const satisfies readonly {
status: string,
title: string
}[]
各コードは次の URL で試せます。
as const satisfies
は人類が本当に求めていたもの
筆者は as const
なオブジェクトを export することがよくありますが、推論結果を保ったまま型で縛りたいとずっと思っていました。TypeScript 4.9 で使えるようになった satisfies
の登場により、as const satisfies
の指定ができるようになったのは嬉しい限り。定数を早速置き換えています。
プロジェクトで as const
なオブジェクトを export する場合は、積極的に使いましょう。
動画・スライド
各種媒体でも、 as const satisfies
について解説しています。
Discussion
ドキュメント読んでもなかなか理解できてなかった部分が多かったので、すごくありがたい記事でした!
活用していきます!
コメントありがとうございます。
私もドキュメントを読んだだけでは理解できなくて、実際に手を動かしてみるとその便利さに気づきました笑
お役に立てたようで何よりです。
わかりやすい記事ありがとうございます!
こちらの例、コメントが逆になっているようでした。
ご指摘ありがとうございます!
その内容自体が古いものだったので、削除しました🙏