🐐

【TypeScript】satisfies演算子について

に公開

はじめに

TypeScriptにおけるsatisfies演算子の概要や使用例、メリット、デメリットについてまとめていきたいと思います。

satisfies演算子とは

satisfies演算子は、オブジェクトや値が特定の型に代入可能かどうかをコンパイル時にチェックするための構文です。チェックは型システム上のもので、実行時には影響しません。簡単な例を以下に示します。

const obj = {
  name: "Alice",
  age: 25,
} satisfies Person;

この例では「objPersonに代入可能かをコンパイル時に検証」します。
一方で、obj自体の型は推論されたまま保持されるのが特徴です。

使用例

定数オブジェクトの例

type Theme = {
  primary: string;
  secondary: string;
};

const theme = {
  primary: "#fff",
  secondary: "#000",
  tertiary: "#aaa", // ← ここでエラー(excess property)
} satisfies Theme;

satisfiesを使うことで、オブジェクトが指定した型の構造に完全に一致しているかをチェックできます。上の例ではtertiaryTheme型に存在しないため、コンパイルエラーになります。これにより、タイポや不要なプロパティを事前に検出できます。

レコード(辞書型)での型チェック

type Role = "admin" | "user" | "guest";

const permissions = {
  admin: ["read", "write", "delete"],
  user: ["read"],
  guest: [],
} satisfies Record<Role, string[]>;

// 各Roleキーが存在するか、型が一致しているかをチェック

もしguestを入れ忘れたり、型を間違えたりすると、コンパイルエラーで検知できます。

ユニオン型での型推論を保持

type Animal = { kind: "cat"; meow: () => void } | { kind: "dog"; bark: () => void };

const animal = {
  kind: "cat",
  meow: () => console.log("meow"),
} satisfies Animal;

// 確認
// 実行時(JavaScript上)では "string" と出力されるが、
// TypeScriptの型システム上では animal.kind は "cat" 型として推論される
console.log(typeof animal.kind); // "string"
console.log("animal.kind の値:", animal.kind); // "cat"
animal.meow(); // エラーなく実行できる

satisfies vs 型注釈(:)

satisfiesと型注釈(: 型名)は似ていますが、挙動が異なります。以下の表にまとめます。

比較項目 satisfies :(型注釈)
型チェック 厳密に行われる 行われる
型推論の保持 保持される 失われる(型が固定される)
エラー検出 型不一致を検出 型不一致を検出するが、型が固定される
type Config = { mode: "dev" | "prod"; debug: boolean };

const config1: Config = {
  mode: "dev",
  debug: true,
};

const config2 = {
  mode: "dev",
  debug: true,
} satisfies Config;

config1はConfigに型を固定するのに対し、config2は「Configに代入可能な構造を持つ推論結果」として扱われます。
そのため config2 のプロパティは、実際のリテラル型(例: "dev")のまま保持されます。

そもそも型推論って???
  • 基本の考え方
    TypeScriptは、コードを見て「この値はこういう型だな」と推測します。
    let userName = "Alice";
    
    型を明示していませんが、TypeScriptは "Alice" が文字列なのでuserName: stringだと自動的に判断します。↓こんな感じ

satisfies vs 型アサーション(as)

比較項目 satisfies as
チェックの有無 型整合性をチェック 型を強制的に変換
安全性 高い 低い(開発者責任)
推論保持 保持される 固定される
type Config = {
  url: string;
  timeout: number;
  retries?: number;
};

// as を使うと、元の型情報が失われる
const config1 = {
  url: "https://api.example.com",
  timeout: 5000,
  retries: 3
} as Config;

config1.retries; // number | undefined 型(オプショナルなので)

// satisfies を使うと、元の型が保持される
const config2 = {
  url: "https://api.example.com",
  timeout: 5000,
  retries: 3
} satisfies Config;

config2.retries; // number 型

as const との組み合わせ

satisfiesas constを組み合わせると、リテラル型推論と型整合性チェックの両方を実現できます。

type Routes = Record<"home" | "about", string>;

const routes = {
  home: "/",
  about: "/about",
} as const satisfies Routes;

これにより、各キーがRoutesに一致しているかを検証し、値はstringリテラル型として固定という安全かつ便利な構造を作れます。なお、as constsatisfies より先に書く必要があります

リテラル型推論ってそもそも何?

型推論の基本

TypeScriptは、変数の値から自動的に型を推測します。

let userName = "Alice";  // userName: string と推論される
const age = 25;          // age: 25 と推論される(リテラル型)

letで宣言すると幅広い型(string)に、constで宣言すると具体的な値の型(25)になります。

オブジェクトや配列でのリテラル型

しかし、constでオブジェクトや配列を宣言しても、中身は広い型になってしまいます。

const config = {
  mode: "dev",
  port: 3000
};
// 型: { mode: string; port: number }
// modeは "dev" ではなく string型になる

as constでリテラル型を保持

「"dev" という具体的な値のまま型として扱いたい」場合は as const を使います。

const config = {
  mode: "dev",
  port: 3000
} as const;
// 型: { readonly mode: "dev"; readonly port: 3000 }
// modeは "dev" リテラル型として保持される

配列でも同様です:

const colors = ["red", "blue"];           // string[]
const colors2 = ["red", "blue"] as const; // readonly ["red", "blue"]

これがリテラル型推論を保持するということです。as constを使うことで、より厳密で安全な型定義ができます。

メリット

  • 型安全性の向上 → 定義と実装の不整合をコンパイル時に検出できる。

  • 型推論を保持 → asや:では失われがちなリテラル推論を保てる。

  • 設定オブジェクトやマップ構造に最適 → Record型や定数定義の整合性チェックに強い。

  • より明確な意図表示 → 「このオブジェクトはこの型に準拠している」と明示できる。

デメリット

  • TypeScript 4.9以降でしか使えない(古いバージョンでは使えない。)
  • シンプルなケースでは冗長に感じる場合がある

まとめ

  • satisfies は型推論を保ったまま、型の整合性を厳密にチェックできる。

  • :as の代替として、安全かつ意図の明確な方法を提供する。

  • 特に設定オブジェクト・マッピング・リテラル定義などで威力を発揮する。

Discussion