0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeScriptの satisfies 演算子を使いこなす ─ 型推論を壊さずに型チェックする方法

0
Posted at

TypeScriptの satisfies 演算子を使いこなす ─ 型推論を壊さずに型チェックする方法

TypeScript を書いていて、こんな経験ありませんか。

オブジェクトに型アノテーションをつけたら、推論が効かなくなって .toUpperCase() が使えなくなった。as で型アサーションしたら、間違った値を書いても怒られなくなった...。

そういう「型をつけたいけど、推論も残したい」という場面に、 satisfies 演算子 がかなりハマります。TypeScript 4.9 で登場したのですが、正直まだあまり使われていない気がします。もったいないなと思って書きました。

この記事で学べること:

  • satisfies が何をするものか(30秒で理解できる説明)
  • 型アノテーション・as constas との違い
  • satisfies が活きる実践的な3パターン
  • よくある落とし穴と使い分けの判断基準

検証環境: TypeScript 5.x


satisfies が解決する問題

まず問題から入ります。

こういうコードを書いたことがあると思います。

// 設定オブジェクトに型をつけたい
type Config = {
  endpoint: string | URL;
  timeout: number;
};

const config: Config = {
  endpoint: "https://api.example.com",
  timeout: 3000,
};

// ここで詰まる
config.endpoint.toUpperCase(); // ❌ エラー: string | URL には toUpperCase がない

Config 型では endpointstring | URL なので、TypeScript は「文字列かもしれないし URL かもしれない」と判断して .toUpperCase() を許してくれません。でも実際には文字列を入れているのにもったいないですよね。

かといって型アノテーションを外すと、型チェックが効かなくなります。

// 型アノテーションなし → 推論は効くが型チェックがない
const config = {
  endpoint: "https://api.example.com",
  timeout: 3000,
};

config.endpoint.toUpperCase(); // ✅ 動く
config.endpoint = 12345; // ❌ 怒ってほしいのに怒ってくれない...

この「 型チェックはしたいけど、推論も失いたくない 」というジレンマを解決するのが satisfies です。


satisfies を使うとこうなる

type Config = {
  endpoint: string | URL;
  timeout: number;
};

const config = {
  endpoint: "https://api.example.com",
  timeout: 3000,
} satisfies Config;

// 両方いける
config.endpoint.toUpperCase(); // ✅ 推論が効いて string として扱われる
config.endpoint = 12345;       // ❌ 型エラー(Config の制約が効いている)

satisfies Config と書くことで、「このオブジェクトは Config 型を満たしているか検証してほしい(でも型は推論したものを使ってほしい)」という意味になります。

一言で言うなら、 「型チェックだけして、型の上書きはしない」 という演算子です。


型アノテーション・as constas との比較

少し整理してみます。

型アノテーション(: Type

const config: Config = { endpoint: "https://...", timeout: 3000 };
// config.endpoint の型 → string | URL(Config の定義に引っ張られる)

型チェックは効く。でも変数の型が Config の定義に上書きされるので、推論が失われます。

as const

const config = { endpoint: "https://...", timeout: 3000 } as const;
// config.endpoint の型 → "https://..."(リテラル型)
// config.timeout の型 → 3000(リテラル型)

推論は最大限効く。でも Config を満たしているかのチェックはしてくれません。

as(型アサーション)

const config = { endpoint: 123, timeout: "hello" } as Config;
// ❌ 明らかにおかしくても通ってしまう

型チェックをほぼバイパスします。緊急回避以外では使わないほうがいいです。

satisfies

const config = { endpoint: "https://...", timeout: 3000 } satisfies Config;
// config.endpoint の型 → string(推論が効いている)
// Config を満たしているかチェック → ✅

型チェックと推論を両立します。

型チェック 推論を保持
: Type
as const ✅(リテラル)
as △(緩い)
satisfies

satisfies が活きる実践パターン

パターン1: 設定オブジェクトの型安全管理

アプリの設定値を型チェックしながら、各プロパティの具体的な型も使いたい場面。

type AppConfig = {
  api: {
    baseUrl: string;
    timeout: number;
  };
  features: {
    darkMode: boolean;
    language: "ja" | "en";
  };
};

const config = {
  api: {
    baseUrl: "https://api.example.com",
    timeout: 5000,
  },
  features: {
    darkMode: true,
    language: "ja",
  },
} satisfies AppConfig;

// 推論が効いているのでメソッドが使える
const upperUrl = config.api.baseUrl.toUpperCase(); // ✅
const doubled = config.api.timeout * 2;            // ✅

// Config を満たさない値はエラー
// config.features.language = "fr"; // ❌ "ja" | "en" 以外はエラー

パターン2: Record 型のオブジェクトで各値の型を保持する

type RouteConfig = {
  path: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
};

const routes = {
  getUser: { path: "/users/:id", method: "GET" },
  createUser: { path: "/users", method: "POST" },
  updateUser: { path: "/users/:id", method: "PUT" },
} satisfies Record<string, RouteConfig>;

// satisfies なしだと routes.getUser の型が RouteConfig になって推論が失われる
// satisfies ありだと各プロパティの型が保持される

// routes.getUser.method の型 → "GET"(リテラル型として推論される)
// routes.createUser.method の型 → "POST"

これが特に便利なのは、 ユニオン型を絞り込んだあとで使いたい 場面です。

function handleRoute(route: typeof routes.getUser) {
  // method の型が "GET" として絞り込まれている
  console.log(route.method); // 型: "GET"
}

型アノテーション(: Record<string, RouteConfig>)を使った場合、routes.getUser.method の型は "GET" | "POST" | "PUT" | "DELETE" になってしまいます。satisfies だと "GET" のまま保持されます。

パターン3: switch 文の網羅性チェックと組み合わせる

type Status = "pending" | "running" | "done" | "error";

type StatusMessage = {
  label: string;
  color: "gray" | "blue" | "green" | "red";
};

// Status の全パターンを網羅しているかチェックしたい
const statusMessages = {
  pending: { label: "処理待ち", color: "gray" },
  running: { label: "処理中", color: "blue" },
  done: { label: "完了", color: "green" },
  error: { label: "エラー", color: "red" },
} satisfies Record<Status, StatusMessage>;

// Status に新しい値を追加したとき、ここでエラーになる → 網羅性の保証
// 例: Status に "cancelled" を追加すると、このオブジェクトが型エラーになる

これはかなり実用的です。 ステータスコードや enum 的なものの対応表を管理するときsatisfies を使っておくと型の追加漏れをコンパイルエラーで検知できます。


よくある落とし穴

推論が思ったより広くなることがある

type Shape = {
  kind: "circle" | "square";
  size: number;
};

const shape = { kind: "circle", size: 10 } satisfies Shape;
// shape.kind の型 → "circle"(推論)
// ただし Shape を経由しているので、"circle" | "square" の範囲内であることは保証される

satisfies を使っても、推論される型は実際の値から決まります。これは意図通りの挙動です。

as const satisfies の組み合わせ

// リテラル型で固定しつつ、型チェックも効かせたい場合
const config = {
  endpoint: "https://api.example.com",
  timeout: 3000,
} as const satisfies Config;

// config.endpoint の型 → "https://api.example.com"(リテラル型)
// Config を満たしているかチェック → ✅

as const satisfies の順番は、 as const が先 です。satisfies as const は文法エラーになります。


使い分けのまとめ

正直なところ、最初は「どこで使うのか」がわかりにくいと思います。判断基準をシンプルにまとめると...

satisfies を使う場面:

  • オブジェクトの各プロパティの推論を保持したまま、型の整合性をチェックしたい
  • Record<K, V> 形式のオブジェクトで、各値の具体的な型を使いたい
  • ユニオン型の全パターンを網羅しているか保証したい

型アノテーション(: Type)を使う場面:

  • 変数の型を明示的に広くしておきたい(柔軟に再代入したい)
  • 推論の具体的な型よりも、インターフェースの型を使いたい

as const を使う場面:

  • 値をリテラル型として固定したい(設定値の変更不可化)

設定オブジェクト、ルート定義、ステータスマッピングなど「 型チェックはしたいが、各プロパティの型も使いたい 」という場面が出てきたら、まず satisfies を試してみると良いと思います。


まとめ

satisfies を一行で表すなら、 「型チェックだけして、型の上書きはしない」 です。

型アノテーションを使ったら推論が消えてしまった、as を使ったらチェックが効かなくなった、そういう経験のある人には刺さると思います。

TypeScript の型システムは年々豊かになっていて、「こう書けばいいのに」という気持ちに応えてくれる機能が少しずつ増えています。satisfies もそのひとつです。知っておくと、型と推論の両立で悩む回数が減る気がします。

// こういうジレンマを感じたら satisfies のことを思い出してください
const config = {
  endpoint: "https://api.example.com",
  timeout: 3000,
} satisfies Config; // 型チェック ✅ 推論も ✅
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?