できない.dev

TypeScript の satisfies 演算子で型が広がる・narrowing が効かない

satisfies は値が型に適合することを検査しつつ値の具体的な型は保持する演算子。
as と違って型を広げないため narrowing も保たれるが、変数の型注釈や const アサーションと併用する位置を誤ると効果が消える。

#satisfies#narrowing#type-inference#const-assertion

公開:

要約

satisfies 演算子は 「この値はこの型に適合するか」を検査しつつ、値の具体的な型はそのまま保持 する。as のように型を強制的に広げないため narrowing が効くが、変数に型注釈を付けると宣言型側が優先されて狭い型情報が失われる。
修正の基本は 変数の型注釈を外し、値の式末尾に satisfies を置く こと。

よくある原因

  1. 型注釈との二重指定: const config: Config = { ... } satisfies Config と書くと const config の型は Config(広い側)になり、リテラル narrowing が消える
  2. as const との順序: { ... } satisfies Config as const は意味が逆。as const satisfies Config の順序にする
  3. 古い TypeScript: satisfiesTypeScript 4.9 で導入。
    4.8 以前ではパースエラー
  4. 検査対象の型が緩い: satisfies Record<string, unknown> のような広い型では narrowing が効かない

解決策

1. 型注釈を外し satisfies に一本化

type Color = "red" | "green" | "blue";
type Palette = Record<string, Color | [number, number, number]>;
 
const palette = {
  red: [255, 0, 0],
  green: "green",
  blue: [0, 0, 255],
} satisfies Palette;
 
palette.green.toUpperCase();
palette.red[0].toFixed();

palette.green"green" リテラルとして narrowing され、palette.red[number, number, number] のタプルのままになる。

2. as const と組み合わせるなら順序に注意

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

as const で全プロパティをリテラル化したうえで、satisfies でテンプレートリテラル型のキー仕様を検査する。
順序を逆にすると型が広がる。

3. 値の具体型を再利用したい場合

const config = {
  port: 3000,
  host: "localhost",
} satisfies { port: number; host: string };
 
type Config = typeof config;

typeof config で narrowing 結果の具体型を取り出せる。type Config = { port: number; host: string } を別に書くと narrowing が失われるので避ける。

4. バージョン確認

npx tsc --version
# 4.9 以上であること

旧バージョンを使っているプロジェクトでは package.jsontypescript を 4.9 以降に上げる。

この記事は役立ちましたか?