📚

改めて学ぶ satisfies 演算子

2024/12/08に公開

TSKaigi Advent Calendar 2024の9日目の記事となります。
https://qiita.com/advent-calendar/2024/tskaigi

皆さんは普段TypeScriptのsatisfies演算子を使っていますか?私はswitch文の最後をnever型で終える際に、1行で記述するために使っていました。

type Role = "admin" | "user";

const getRoleText = (role: Role) => {
  switch (role) {
    case "admin":
      return "管理者";
    case "user":
      return "ユーザー";
    default:
      // もし将来Role型に新しい値が追加された場合、この部分でコンパイルエラーが発生
      return role satisfies never;
  }
};

const getRoleText = (role: Role) => {
  switch (role) {
    case "admin":
      return "管理者";
    case "user":
      return "ユーザー";
    default: {
      // 型アノテーションを使うと2行書かないといけない
      const _: never = role;
      return _;
    }
  }
};

ですがsatisfiesの具体の仕様や、他にどのようなユースケースで使えるのかを詳しく知らなかったためこの機会に調べてみました。本記事ではそれをまとめたいと思います。

satisfies演算子とは?

「値が特定の型を満たすことをチェックしつつ、その値の具体的な型情報は保持する」演算子です。TypeScript v4.9でリリースされました。

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

以下のようにadressとタイポしてしまった時に型チェックで気づけるようにするユースケースを考えます。

// ユーザーの連絡先情報を管理するケース
type ContactType = "email" | "phone" | "address";
type ContactFormat = string | number[];

// タイプミスを検出したいパターン
const userContact = {
    email: "user@example.com",
    phone: [81, 90, 1234, 5678],
    adress: "Tokyo, Japan"  // addressのタイプミス
};

型アノテーションを使うことでタイポを型エラーで気づくことができますが、emailがstringである情報は失われてしまいます。

// 型アノテーションを追加
const userContact: Record<ContactType, ContactFormat> = {
    email: "user@example.com",
    phone: [81, 90, 1234, 5678],
    adress: "Tokyo, Japan"  // ここでタイプミスを検出できる!
};

// しかし新たな問題が発生
// emailがstring | number[]型として解釈されるため、
// 文字列メソッドが使えなくなってしまう
const normalizedEmail = userContact.email.toLowerCase();  // エラー

satisfiesを使うことで上記の問題を解決し、型チェックしつつ具体的な型情報は保持されます!

// satisfiesを利用
const userContact = {
    email: "user@example.com",
    phone: [81, 90, 1234, 5678],
    adress: "Tokyo, Japan"  // ここでタイプミスを検出できる!
} satisfies Record<ContactType, ContactFormat>;

// emailは文字列型として認識されるので、文字列メソッドが使える
const normalizedEmail = userContact.email.toLowerCase();

as constと併用するパターン

as constとsatisfiesを組み合わせることで、より厳密な型チェックと型推論が可能になります。先ほどの例でas constと併用した場合について考えてみましょう。

emailはsatisfiesのみを利用した場合はstring型として推論されましたが、as constを併用することで文字列リテラル型として推論されます。

// as constとsatisfiesを併用
const userContact = {
  email: "user@example.com",
  phone: [81, 90, 1234, 5678],
  address: "Tokyo, Japan",
} as const satisfies Record<ContactType, ContactFormat>;

// これにより:
// 1. emailは具体的な文字列リテラル型 "user@example.com" として扱われる
// 2. phoneは readonly [81, 90, 1234, 5678] という固定された配列型として扱われる
// 3. addressは "Tokyo, Japan" という文字列リテラル型として扱われる

// さらに、値の変更も防止される
userContact.phone[0] = 1; // エラー:読み取り専用の配列
userContact.email = "new@example.com"; // エラー:読み取り専用のプロパティ

より実践的なユースケースで考えてみましょう。カラーコードなどの定数はstring型ではなく文字列リテラルで不変な値を取得しつつ、カラーコードの記法を型チェックすることができます。

type Theme = "light" | "dark";
type ColorCode = `#${string}`;

const themeColors = {
    light: {
        primary: "#007bff",
        secondary: "#6c757d",
        background: "#ffffff"
        
    },
    dark: {
        primary: "#375a7f",
        secondary: "#444444",
        background: "#222222"
    }
} as const satisfies Record<Theme, Record<string, ColorCode>>;

// 利点:
// 1. 各色の値が固定され、変更できない
// 2. 各色の値が正確な文字列リテラル型として推論される
// 3. テーマのキーが Theme 型にマッチするか検証される
// 4. 色コードが ColorCode 型にマッチするか検証される

// 型安全な色の取得が可能
const lightPrimary: "#007bff" = themeColors.light.primary;

2024年11月に発売されたJavaScriptプログラマーのためのTypeScript厳選ガイド 〜JavaScriptプロジェクトを型安全で堅牢にする書き方を理解するの中でもas constとの併用について解説されており参考にさせていただきました。

使い所

現職のWebアプリケーション開発において、どのようなシーンで「特定の型を満たすことをチェックしつつ、その値の具体的な型情報は保持したいか?」を考えていきます。

Enumライクなオブジェクトの型安全性

以下のような条件でEnumライクな定数を定義したい場合があります。

  • この定数が将来安全に拡張されるよう保証したい
  • 利用する時にはリテラルで推論してほしい

型アノテーションではなくsatisfiesを使うことで条件を満たした定義が可能です!

// Enumライクなオブジェクトの型安全性
const COLORS = {
  RED: "#ff0000",
  GREEN: "#00ff00",
  BLUE: "#0000ff"
} as const satisfies Record<string, `#${string}`>;

// コンパイル時の型チェック
const color: "#ff0000" = COLORS.RED;  // OK
const color2: "#ff0001" = COLORS.RED; // エラー:型が一致しない

// hexカラーコードの形式チェック
const INVALID_COLORS = {
    RED: "ff0000",    // エラー:#で始まっていない
    GREEN: "green"    // エラー:正しいhex形式ではない
} as const satisfies Record<string, `#${string}`>;

ネストしたオブジェクトの定義

Enumライクな定数に近いですが、オブジェクトをネストさせてパターンを表現し、随時拡張されていく定義を書きたい場合があります。同様にas constと併用することで型推論しつつ、コード補完で具体の値を知ることができ可読性を向上することができます。

// APIのエンドポイント定義

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = { method: Method; path: string; requiresAuth: boolean };

const API_ENDPOINTS = {
    getUser: {
        method: 'GET',
        path: '/api/users/:id',
        requiresAuth: true
    },
    updateProfile: {
        method: 'PUT',
        path: '/api/users/:id/profile',
        requiresAuth: true
    },
    login: {
        method: 'POST',
        path: '/api/auth/login',
        requiresAuth: false
    }
} as const satisfies Record<string, Endpoint>;

// 型推論とコード補完が効く
const userPath = API_ENDPOINTS.getUser.path;  // '/api/users/:id'として推論
const needsAuth = API_ENDPOINTS.login.requiresAuth;  // falseとして推論

余剰プロパティに対しての挙動(追記)

公式の解説ではColors型を例に、余剰プロパティがエラーとなる例について紹介されています。

type Colors = "red" | "green" | "blue";
// Ensure that we have exactly the keys from 'Colors'.
const favoriteColors = {
    "red": "yes",
    "green": false,
    "blue": "kinda",
    "platypus": false
//  ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'.
} satisfies Record<Colors, unknown>;

この仕様はsatisfiesに限った話ではなく、型アノテーションを利用した場合も同様です。 [1]

type User = {
  name: string;
  age: number;
};

// satisfiesを使った場合
const user = {
  name: "John",
  age: 30,
  email: "user@example.com", // エラー
} satisfies User

// 型アノテーションを使った場合
const user: User = {
  name: "John",
  age: 30,
  email: "user@example.com", // エラー
};

余剰プロパティのチェックはリテラルの直接代入の時にのみ有効で、変数代入時にはエラーにならない仕様も同様です。

const user = {
  name: "John",
  age: 30,
  email: "user@example.com",
};

// 変数での代入の場合はエラーにならない
const user1: User = user;
const user2 = user satisfies User;

詳しくはこちらの解説をご確認ください。

終わりに

いかがでしたでしょうか?私自身も今回の記事を通じて、普段何気なく書いていた演算子の仕様を詳しく知ることができました。明日は@nanaonanikaさんによるTypeScript中級者向け!型レベルプログラミングの紹介です!

TSKaigiの告知

2025年5月23日/24日にTSKaigi 2025が開催されます!
TSKaigiは日本最大級のTypeScriptをテーマとした技術カンファレンスです(前回の参加者2000人以上)
TypeScriptに興味のある方は、ぜひ公式サイトやXを確認してみてください。

TSKaigi 2025 ティザーサイト:https://2025.tskaigi.org/
公式サイト:https://tskaigi.org/
X:https://x.com/tskaigi

参考

脚注
  1. 訂正とお詫び
    記事の公開時には誤った例を掲載していたため修正と追記を行いました。既にご覧になられた方は申し訳ございません。 ↩︎

Discussion