Gemcook Tech Blog
😆

【TS】satisfiesじゃないとできないこと。 ~Uppercase<T>と一緒に型を強化する~

2024/06/26に公開

TypeScript4.9 に実装されたsatisfiesオペレータですが、どうでしょう。みなさん使ってますか?
僕はもちろん愛してやまないのですが、satisfiesの実装から1年半くらいたった今、実装当初に想定していたよりも、「あれ?こいつここでも使えるの?」と活躍を感じる場面が多くなってきたため、いいなと思った使用例を紹介したいと思います。

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

satisfiesの基本

今回の記事ではsatisfiesそのものについての解説は行いません。以下の記事がとても丁寧かつわかりやすくまとめられていますので、おすすめです!

https://zenn.dev/tonkotsuboy_com/articles/typescript-as-const-satisfies

よろしければこちらもどうぞ...。

筆者の旧アカウントでの記事です。よろしければどうぞ!
https://zenn.dev/kanachan/articles/1684b3721971d1

Uppercase<T>と組み合わせて型を強化する

今回紹介したいTipsは、
satisfiesUppercaseを一緒に使って、いつもの型をもう少し強化しよう」 です!
例えば、COLORオブジェクトのkey部分をUPPER_CASEで宣言するよう型で縛りたい時を考えてみましょう。型として表現すると以下のようになります。

type ColorKey = UpperCase<string>

上記のColorKey型を利用して、「key部分はUPPER_CASEに縛られたCOLORオブジェクト」を作ってみます。以下の2パターンが思い付きます。

// 型注釈を利用
const COLOR_ANNOTATE: Record<ColorKey,string> = {
    WHITE: "#FFFFFF",
    BLACK: "#000000",
    badColor: "#hoge" // 🚫 type error ("badColor"がUPPER_CASEじゃない為)
}

// satisfiesを利用
const COLOR_SATISFIES= {
    WHITE: "#FFFFFF",
    BLACK: "#000000",
    badColor: "#hoge" // 🚫 type error ("badColor"がUPPER_CASEじゃない為)
} satisfies Record<ColorKey,string>

さて、これらはどちらも期待通り動作しており一見同じものに見えますが、違いはあるのでしょうか...🤔?
...当然あります、そして当然satisfiesを使用している方が有利です。(当たり前ですね。だって記事書いてますもん。)

何が違う?

何が違うのかみていきましょう。結論から言うと、
「オブジェクトのプロパティへ型安全にアクセスできるかどうか」
において違いが生まれます。


// 型注釈を利用
const COLOR_ANNOTATE: Record<ColorKey,string> = {
    WHITE: "#FFFFFF",
    BLACK: "#000000",
    badColor: "#hoge" // 🚫 type error
}

// satisfiesを利用
const COLOR_SATISFIES= {
    WHITE: "#FFFFFF",
    BLACK: "#000000",
    badColor: "#hoge" // 🚫 type error
} satisfies Record<ColorKey,string>

// 型安全にアクセス不可(サジェストしてくれない👎)
const selectedColorAnnotate = COLOR_ANNOTATE.????

// 型安全にアクセス可能(サジェストしてくれる👍)
const selectedColorSatisfies = COLOR_SATISFIES.BLACK

なぜこんなことが起こるのか少し詳しくみていきたいと思います。

COLOR_ANNOTATE の型

COLOR_ANNOTATEオブジェクトの型は型注釈を行っているため
Record<Uppercase<string>, string>型です。
そしてUppercase<string>は結局のところstringなため、最終的に、

/*
{[x: string]: string;}
*/
typeof COLOR_ANNOTATE

となります。
keyが単なるstring型な為、どんな文字列でも指定することができてしまうので、安全にアクセスできないという訳ですね。

COLOR_SATISFIES の型

対して、COLOR_SATISFIESオブジェクトの型はどうなるでしょうか。
satisfiesを使用していますが、型注釈は行なっていない為、実際のオブジェクトの型は型推論により行われます。その為最終的に

/*
{
    WHITE: string;
    BLACK: string;
{
*/
typeof COLOR_SATISFIES

となります。
実際の値に基づいて型推論されるため、keyの型が具体的な stringLiteral型となり、型安全に各プロパティにアクセスできるようになる訳ですね。

その他の型と組み合わせる

Uppercaseの仲間たち

上記では一例としてUppercase<T>と組み合わせる例を出しましたが、他にも以下のような型関数が存在します。

  • Lowercase<T>
  • Capitalize<T>
  • Uncapitalize<T>

これらが何をしてくれる型関数なのかは、読んで時の如くです。
これらの宣言方法であるintrinsicキーワードについて詳しく知りたい方は、uhyoさんのこの記事が参考になるかと思います。(NoInferも最近intrinsicの仲間になりましたね😸)

https://zenn.dev/uhyo/articles/typescript-intrinsic

TemplateLiteral型

上記の型関数以外にも、TemplateLiteral型とsatisfiesを組み合わせて使うのも良いかと思います。
Uppercaseの例と同じですが、以下のようなイメージです。

type ErrorMessageKey = `${number}_Error`

const ErrorMessage = {
    "404_Error": "ご指定のページが見つかりませんでした",
    "418_Error": "ティーポットなのでコーヒーはちょっと淹れんといてほしい...",
} satisfies Record<ErrorMessageKey, string>

...色々と使えそうな場面は多そうですね!

さいごに

さて、satisfiesのTipsを紹介しました。
今回紹介した以外にも、いくつかお気に入りのsatisfies利用法がありますので、別記事で紹介できたらなぁと思っています。僕が満足するまでお付き合いをどうぞよろしく!!

一応Playgroundも用意しているので、実際に確認してみてください...!!

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAwg9gGzgJwNIRFAvFAqmSZAYwEMBnCAHjOGQEsA7AcwD4AoNgek6kGj1QC5tAE4mAkhkCUSoApXNkTgMasAPIAZeQCUA+gEEAclvkAVDXoCiALigqI05ABNK8JGgwAaGvWYtsUAN5sofqADqABIAksZmAEQAxABicfERTr7+AEKKGjCokVEADHn5icl+AEYk1vYo2QCMNbURUNxQgLwbgNV7UKCQUBDIyChQABQRpeWIKBGAMgy4AApTRuowGgDKRoDmDID2DIBWDIAiDID6DICBDACUUGwAvhyNZCTAdGQAZnQQZGKS0rLACsrqi4YhizEhRkWOB8-kCoXCUGi8QSSVBaQyWUhuXyeUKoOGFWQ1VqNXqjVa7XA0G6vWQAyGZUxE2ms3mS1Wm12h2OJygl2udweZHMlhQtkx6BALlojFYHA4rzkFAQlmAEBGDg0DAYcGAV2gOBgSlUmh0+kMRgAdAB+U3GqQyKUQGVEOUKlCLK43e6PTxaz5qb56X7-QGG+GZIA

ありがとうございました!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion