Article

🧩 TypeScriptのasを減らす:satisfiesとの使い分けを実例で整理する

TypeScript, JavaScript, 型安全, フロントエンド

TypeScriptを書いていると、型エラーを消すために as を使いたくなる場面があります。

ただし、as は便利な一方で、使い方によってはTypeScriptの型チェックを自分で弱めてしまいます。

一方、TypeScript 4.9で追加された satisfies は、「この値が指定した型を満たしているかチェックする」一方で、元の具体的な型推論をできるだけ維持するための演算子です。

この記事では、as、型注釈、satisfiesas const の違いを、実務で使いやすい形で整理します。

先に結論

迷ったときは、だいたい次の順番で考えると安全です。

  1. まずはTypeScriptの型推論に任せる
  2. オブジェクトが特定の型を満たすことを確認したいなら satisfies
  3. 変数そのものの型を固定したいなら型注釈 : Type
  4. リテラル値をそのまま保持したいなら as const
  5. コンパイラより自分の方が型を詳しく知っている場合だけ as

as は「型を検証する」というより、コンパイラに対して『この型として扱ってよい』と伝える機能です。

as は型チェックではなく型アサーション

TypeScript公式ドキュメントでは、asType Assertion(型アサーション) と呼ばれています。

例えば次のコードです。

const element = document.getElementById("canvas") as HTMLCanvasElement;

document.getElementById() の戻り値は、TypeScriptから見ると HTMLElement | null です。

しかし開発者が「このIDの要素は必ず HTMLCanvasElement だ」と分かっている場合、as HTMLCanvasElement と書くことで型を絞れます。

重要なのは、as が実行時の値を変換しているわけではないことです。

const value = "100" as unknown as number;

console.log(typeof value);
// "string"

TypeScript上では number として扱えても、JavaScriptとして実行された値は文字列のままです。

つまり as は、Javaのキャストのように値そのものを変換する処理ではありません。

TypeScript公式ドキュメントでも、型アサーションはコンパイル時に取り除かれ、実行時チェックを行わないと説明されています。

as で型エラーを消すのが危険な理由

例えばAPIレスポンスを扱うコードを考えます。

type User = {
  id: string;
  name: string;
};

const response = {
  id: "123",
};

const user = response as User;

このコードはコンパイルできる場合がありますが、実際には name がありません。

console.log(user.name.toUpperCase());

実行時にはエラーになる可能性があります。

TypeScriptが本来教えてくれるはずだった不整合を、as によって開発者側で握りつぶしてしまった状態です。

そのため、次のようなコードをレビューで見たら、一度立ち止まって考える価値があります。

something as SomeType

本当にTypeScriptより詳しい情報を持っているのか、それとも単に型エラーを消したいだけなのか、という点です。

satisfies は「型を満たすか」をチェックする

ここで使いやすいのが satisfies です。

type Config = {
  apiUrl: string;
  retryCount: number;
};

const config = {
  apiUrl: "https://example.com",
  retryCount: 3,
} satisfies Config;

この場合、configConfig を満たしているかTypeScriptが検証します。

例えばプロパティを間違えるとエラーになります。

const config = {
  apiUrl: "https://example.com",
  retryCount: "3",
} satisfies Config;

retryCountnumber である必要があるので、コンパイル時に検出できます。

as Config で強制的に型を合わせるのとは、目的が逆です。

// 「Configとして扱う」
const config1 = value as Config;

// 「Configを満たしているか確認する」
const config2 = value satisfies Config;

この違いを覚えておくと使い分けやすくなります。

型注釈と satisfies の違い

では、普通に型注釈を書けばよいのではないでしょうか。

type Status = "success" | "error";

type ApiConfig = {
  status: Status;
  retryCount: number;
};

const config: ApiConfig = {
  status: "success",
  retryCount: 3,
};

もちろん、この書き方も正しいです。

ただし型注釈を付けると、変数は指定した型として扱われます。

satisfies は、型との互換性をチェックしつつ、値から得られる具体的な型情報を維持しやすいのが特徴です。

TypeScript公式の例では、次のようなケースが紹介されています。

type Colors = "red" | "green" | "blue";
type RGB = [number, number, number];

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<Colors, string | RGB>;

palette.green.toUpperCase();

paletteRecord<Colors, string | RGB> を満たすことをチェックしつつ、green が具体的に文字列であるという情報も利用できます。

これが satisfies のかなり便利なポイントです。

オブジェクトのキー漏れ・タイプミスを検出する

設定値や定数テーブルでは特に便利です。

type Environment = "development" | "staging" | "production";

const apiUrls = {
  development: "http://localhost:3000",
  staging: "https://stg.example.com",
  production: "https://example.com",
} satisfies Record<Environment, string>;

例えば production を書き忘れるとTypeScriptが検出します。

const apiUrls = {
  development: "http://localhost:3000",
  staging: "https://stg.example.com",
} satisfies Record<Environment, string>;

逆に、存在しないキーを追加しても検出できます。

const apiUrls = {
  development: "http://localhost:3000",
  staging: "https://stg.example.com",
  production: "https://example.com",
  prodution: "https://typo.example.com",
} satisfies Record<Environment, string>;

設定ファイルのタイプミスは実行時まで気づきにくいので、こうした用途では satisfies と相性が良いです。

ReactやNext.jsでも使いやすい

例えば画面ごとの設定をまとめるケースです。

type PageName = "home" | "settings" | "profile";

type PageConfig = {
  title: string;
  requiresAuth: boolean;
};

const pages = {
  home: {
    title: "ホーム",
    requiresAuth: false,
  },
  settings: {
    title: "設定",
    requiresAuth: true,
  },
  profile: {
    title: "プロフィール",
    requiresAuth: true,
  },
} satisfies Record<PageName, PageConfig>;

新しいページをUnion型に追加したのに設定を追加し忘れた場合、コンパイル時に気づけます。

type PageName = "home" | "settings" | "profile" | "billing";

billingpages に存在しなければエラーになるため、変更漏れを防ぎやすくなります。

as const とは役割が違う

satisfies と一緒によく見かけるのが as const です。

const config = {
  mode: "production",
} as const;

通常、オブジェクトの modestring と推論される場面があります。

as const を付けると、より具体的なリテラル型を保持できます。

// "production" として扱われる
config.mode;

さらにオブジェクトのプロパティは readonly 扱いになります。

一方 satisfies の目的は、対象が指定した型を満たすか検証することです。

type AppConfig = {
  mode: "development" | "production";
};

const config = {
  mode: "production",
} satisfies AppConfig;

必要に応じて組み合わせることもできます。

const routes = {
  home: "/",
  settings: "/settings",
} as const satisfies Record<string, string>;

ただし as const を付けると readonly になるため、「後から値を書き換えたいオブジェクト」では意図に合っているか確認が必要です。

外部データの検証には satisfies だけでは足りない

ここは重要です。

APIレスポンスや localStorage、ユーザー入力など、実行時に外部から入ってくる値は satisfies だけでは安全になりません。

例えば次のデータです。

const response = await fetch("/api/users/1");
const data = await response.json();

response.json() の中身は実行してみるまで分かりません。

こうした値は、Zodなどのスキーマバリデーションライブラリや、自前のType Guardを使って実行時にも検証する必要があります。

function isUser(value: unknown): value is User {
  if (typeof value !== "object" || value === null) {
    return false;
  }

  return (
    "id" in value &&
    typeof value.id === "string" &&
    "name" in value &&
    typeof value.name === "string"
  );
}

TypeScriptの型情報は基本的にコンパイル時のものです。

「TypeScriptで型が付いている = APIから来る値も必ず正しい」という意味ではありません。

実務での使い分け

自分なら、次の基準で使い分けます。

型推論に任せる

特に制約を掛ける必要がない場合です。

const user = {
  id: "123",
  name: "Taro",
};

不要な型指定を増やさない方がシンプルです。

satisfies

設定オブジェクト、マッピング、定数テーブルなどで、構造をチェックしたい場合です。

const config = {
  // ...
} satisfies Config;

型注釈 : Type

変数をその型として扱うこと自体に意味がある場合です。

const config: Config = {
  // ...
};

as

DOM APIなど、TypeScriptが取得できない情報を開発者が確実に把握している場合です。

const canvas = document.getElementById("canvas") as HTMLCanvasElement;

ただし null の可能性まで消してよいかは別途確認が必要です。

as const

文字列や数値を広い型にせず、リテラル型として保持したい場合です。

const methods = ["GET", "POST"] as const;

レビューで as を見つけたときのチェックポイント

as 自体が悪いわけではありません。

ただし、次の3点を確認すると危険な型アサーションを減らしやすくなります。

  • as を外すとどんな型エラーが出るか
  • そのエラーを本当に無視してよい根拠があるか
  • satisfies、型ガード、型設計の見直しで解決できないか

特に、次のような二重アサーションは注意が必要です。

value as unknown as User

TypeScriptの安全装置をかなり強く回避しているため、本当に必要な理由をコード上またはレビューで説明できる状態にした方がよいです。

まとめ

assatisfies は似て見えますが、役割はかなり違います。

  • as:この値を指定した型として扱うようTypeScriptに伝える
  • satisfies:この値が指定した型を満たしているか確認する
  • 型注釈:変数そのものを指定した型として扱う
  • as const:値をより具体的なリテラル型として保持する

型エラーが出たときにすぐ as を追加するのではなく、まず「なぜTypeScriptがエラーを出しているのか」を確認するだけでも、実行時バグを減らしやすくなります。

設定オブジェクトや定数テーブルなら、まず satisfies を試してみるのがおすすめです。

参考資料