📚

ユースケースから学ぶ satisfies

に公開

この記事は何か

typescript 4.9satisfies operator がリリースされています。

しかし、typescript 全体からみると比較的新しい? ためか、あまりまだ satisfies が有効な場面で使われていない印象を受けるレポジトリが多いです。
またこれに間接的に関係するのか、AI が提出するコードも、satisfies を交えたコードをあまり提出してきません。

satisfies についての説明は web に多々あれど、いまいち使い所がわからない方のための記事です.

前提知識

  1. 「immutable な変数とは何か」を把握していること
  2. as const を理解していること

satisfies とは

ケース1: 定数定義 - 定義名と中身に制約を設けたい

もっとも単純で、恩恵がわかりやすく、すぐに実践できる内容です.

  • ケース:ユーザーのロール名を定数管理している場合
  • 定数は大文字で定義して、実値は小文字で統一したい場合

初期値

export const ROLE = {
  ADMIN: "admin",
  EDITOR: "editor",
}

上記の場合、推論はこうなります

type ROLE: {
    ADMIN: string;
    EDITOR: string;
}

as const をつける

as const をつけると

export const ROLE = {
  ADMIN: "admin",
  EDITOR: "editor",
- }
+ } as const

下記のように推論されます.

type ROLE: {
    readonly ADMIN: "admin";
    readonly EDITOR: "editor";
}

これにより、 ROLE.ADMIN とした時に、 VSCode ならカーソルホバーしたときなどに中身が admin であることがわかります。

satisfies をつける

  • 定数は大文字で定義して、実値は小文字で統一したい場合

という制約を satisfies で実現します。

export const ROLE = {
  ADMIN: "admin",
  EDITOR: "editor",
- } as const
+ } as const satisfies {[key in string]: Lowercase<key>}

推論結果は変わりません。ただし satisfies があるなしで、 ROLE という定数が

  • 定数は大文字で定義して、実値は小文字で統一

されているかの保証の有無が変わります。
例えば

export const ROLE = {
  ADMIN: "admin",
-  EDITOR: "editor",
+  EDITOR: "Editor";
} as const satisfies {[key in string]: Lowercase<key>}

とすると、型エラーになります

↓↓↓ 実際に試してみるのが一番わかりやすい ↓↓↓
https://www.typescriptlang.org/play/

ケース2: switch 分の網羅性担保

こちらも単純で、恩恵がわかりやすく、すぐに実践できる内容です。
全ての switch 文で実装すべきです。

こちら参照

ケース3: テストコード - 依存インスタンスがでっかいとき

テストコード記述する際、

  • あるインスタンスを mock する必要がある
  • しかし、mock するそのインスタンスのパラメータが非常に多い。
  • だが、実際にそのインスタンスのうち実際に使うメソッドはほんの一部.テストのためにインスタンスすべての値を定義するのは無駄が大きい

具体的に、

  • ある関数が内部で AWS DynamoDB の Client を利用している

とした場合、jest による mock で、一例として下記のようなコードを書くことになります

import * as AWS from "aws-sdk"

const mockDynamoDBClient = {
  update: jest.fn().mockReturnValue({
    promise: mockUpdatePromise,
  }),
} as unknown as AWS.DynamoDB.DocumentClient

:::message warn
AWS.DynamoDB.DocumentClientupdate 以外にも大量のメソッドを持っており、仮の値を入れるのは非現実的😟
:::

とりあえずこれで

  • あるインスタンスを mock する必要がある

は実現できます。が、

  • ⚠️ as AWS.DynamoDB.DocumentClient による形と実際の値の不一致

を許してしまします。言い換えると

  • ⚠️ AWS.DynamoDB.DocumentClient で定義されていないメソッド名で mock してしまう可能性がある

ということです。例えば、下記のように変更しても、形エラーが発生しません. 事故りかねません ⚠️

import * as AWS from "aws-sdk"

const mockDynamoDBClient = {
-   update: jest.fn().mockReturnValue({
+   updates: jest.fn().mockReturnValue({
    promise: mockUpdatePromise,
  }),
} as unknown as AWS.DynamoDB.DocumentClient

ということで、 as constsatisfies を導入するとこうなります

import * as AWS from "aws-sdk"

const mockDynamoDBClient = {
  update: jest.fn().mockReturnValue({
    promise: mockUpdatePromise,
  }),
- } as unknown as AWS.DynamoDB.DocumentClient
+ } as const satisfies Partial<AWS.DynamoDB.DocumentClient>

これにより、

  • AWS.DynamoDB.DocumentClient での型定義に沿った上で (satisfies の効果)
  • ✅ 必要最低限のメソッドのみを mock 記述して (satisfiesPartial の効果)
  • mockDynamoDBClient を使うときは update しか参照できない = mock 定義してないメソッドを参照する事故をなくす (as const の効果)

の恩恵を受けます。いいことしかないです

まとめ

プロジェクト内で satisfies がまだ浸透していない場合、PR で使ったコード提出してドヤりましょう。 typescript 4.9 以上です。
他にも「こんなケースで嬉しいよ」があれば記事更新しておきます。

Discussion