TS 4.9 が使えない環境に送る satisfies ヘルパー関数
みなさん satisfies operator は使ってますか?
この記事では、satisfies が手に馴染んだけどまだ TS 4.9 に移行できていない環境で作業している人に向けた satisfies を代替するヘルパー関数を紹介します
satisfies とはなにか
satisfies operator は TS 4.9 で追加された演算子です
型注釈には
- 変数等の型を注釈した型に推論させる
- 注釈した型を満たすように制約を設ける
の 2 つの効果があり、制約の効果のみ取り出したものが satisfies operator です
いつ制約だけあると嬉しいかと言うと、制約よりも値(宣言)のほうが厳格なケースです
type ILang = {
lang: string
}
const valueAnnotation: ILang = {
lang: "typescript",
} as const // : ILang
この場合、string
よりも 'typescript'
のほうが厳格なので 'typescript'
に推論されると嬉しいですが、型注釈では string
に推論されます
一方 satisfies で制約のみ課せば
const value = {
lang: "typescript",
} as const satisfies ILang // :{ readonly lang: "typescript" }
'typescript'
に推論されて望ましいです
また、例えば「リポジトリ層の関数の戻り値への制約として 正常系 | BaseError
を返す」があるとして
class BaseError extends Error {}
class SomeError extends BaseError {}
type IRepository<Args extends unknown[], Ret> = (
...args: Args
) => Promise<Ret | BaseError>
declare const db: string[]
const langRepository = (async (lang) => {
if (!db.includes(lang)) {
return new SomeError()
}
return { lang }
}) satisfies IRepository<[string], ILang>
satisfies で制約だけ貸すと、異常系が具体の型に推論されて望ましいね、みたいなこともできますね
あとは変数に代入せずともチェックできるので、網羅チェックも未定義変数を用意せずにかけます
declare const union: "typescript" | "rust" | "go"
switch (union) {
case "typescript":
break
case "rust":
break
case "go":
break
default:
union satisfies never
}
satisfies を代替する関数
ということで、satisfies を使いたくなりますが、TS 4.9 まであげないと使うことができません
わざわざ注釈書くときって、僕の場合は制約が欲しくて書いていることがほとんどなので、最近は基本 satisfies が使いたいなという気持ちなんですが、残念なことに TS 4.9 まであげないと使うことができません
ただし、satisfies と同様の役割を果たすヘルパーを作ることはできます
export const satisfies =
<T extends unknown>() =>
<U extends T>(value: U) =>
value
使い方としては T の型引数は明示的に指定して、U は推論させます(明示しません)
const value = satisfies<ILang>()({
lang: "typescript",
} as const)
補足すると、型引数 T
は明示しているので当然 ILang
型に解決されます。U
は extends ILang
によって ILang
の制約がかかりますが、ちょうど ILang
に推論されるわけではなく渡された引数 value の型に推論されます
これで擬似的に推論は値にまかせて制約だけ課すことができます
具体の型に推論されますし
制約を満たさない場合には型エラーが発生します
冒頭で紹介した 2 パターンも satisfies 関数で書くことができます
// : (lang: string) => Promise<ILang | SomeError>
const langRepositoryWithSatisfiesFn = satisfies<IRepository<[string], ILang>>()(
async (lang) => {
if (!db.includes(lang)) {
return new SomeError()
}
return { lang }
}
)
declare const union: "typescript" | "rust" | "go"
switch (union) {
case "typescript":
break
case "rust":
break
case "go":
break
default:
// 網羅されていないとエラーが出る
satisfies<never>()(union)
}
まとめ
この記事では TS 4.8 以下で satisfies を代替するヘルパー関数を紹介しました
この記事で紹介しているサンプルコード・ヘルパー関数は以下のプレイグラウンドで試すことができます
Discussion