- TypeScript
TypeScriptのas constとsatisfiesの違いを比較表で整理
as const は「値をできるだけ細かい型(リテラル型+readonly)で推論させる」書き方、satisfies は「推論された型はそのままに、指定した型に合っているかだけを検査する」演算子です。 役割が別なので、どちらか一方を選ぶものではなく、as const satisfies 型 と組み合わせると「細かい型」と「型の検査」の両方が手に入ります。
この記事では、型注釈・as・as const・satisfies・as const satisfies の5パターンを同じオブジェクトに書き分け、推論される型と出るエラーメッセージを1つの表に並べました。 型はすべて TypeScript 7.0.2(2026年10月時点の npm の latest)で tsc --declaration の出力を確認したもので、エラー文もコンパイラの出力をそのまま載せています。後半には、設定オブジェクト・ルート定義・enum の代替・配列から union 型を作る、といったそのまま貼れるコードをまとめました。
as constとsatisfiesの早見表
| 書き方 | 推論される型 | 型の検査 | 書き換え | 向いている場面 |
|---|---|---|---|---|
const x: T = 値(型注釈) |
T に広がる(値の細かい情報は残らない) | あり(厳密) | T の定義どおり | 関数の引数・戻り値、あとから中身を入れ替える変数 |
値 as T(型アサーション) |
T に上書き | ゆるい(不足プロパティが通る) | T の定義どおり | 外から来た値など、コンパイラより自分のほうが型を知っているとき |
値 as const |
リテラル型+すべて readonly | なし(型を指定しないため) | できない(コンパイル時) | 定数、union 型の元になる配列・オブジェクト |
値 satisfies T |
値から推論した型のまま | あり(厳密) | できる | キー名を残したい設定、型の検査だけしたいとき |
値 as const satisfies T |
リテラル型+readonly のまま | あり(厳密) | できない(コンパイル時) | 定数の定義全般(設定・ルート・マスタ) |
迷ったときの決め方は次の3行です。
- 定数(あとで書き換えない値)を定義する →
as const satisfies T - 書き換える予定のあるオブジェクトで、キー名や値の型を残したい →
satisfies T - 変数の型を T そのものにしたい(あとで別の値を入れる) → 型注釈
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScriptのas constとsatisfiesの違いを比較表で整理」
確認した環境
| 項目 | 内容 |
|---|---|
| TypeScript | 7.0.2(npm i typescript で入る latest。npx tsc -v で確認) |
| Node.js | 22.22.2 |
| コンパイルオプション | --strict --target es2022 |
| 型の確認方法 | tsc --declaration --emitDeclarationOnly で出力した .d.ts(エディタのホバー表示に相当) |
satisfies は TypeScript 4.9、as const は 3.4、const 型パラメータは 5.0 で入った機能です。5.0 以降であれば、この記事のコードはそのまま使えます。
5パターンを同じコードで比べる
まず、同じオブジェクトを5通り(+何も付けない場合)で書きます。Theme は「キーは自由、値は色名か RGB の3要素」という型です。
// five.ts
type Color = "red" | "green" | "blue";
type Theme = Record<string, Color | readonly [number, number, number]>;
// 0. 何も付けない
export const z = { primary: "red", accent: [0, 128, 255] };
// 1. 型注釈
export const a: Theme = { primary: "red", accent: [0, 128, 255] };
// 2. as
export const b = { primary: "red", accent: [0, 128, 255] } as Theme;
// 3. as const
export const c = { primary: "red", accent: [0, 128, 255] } as const;
// 4. satisfies
export const d = { primary: "red", accent: [0, 128, 255] } satisfies Theme;
// 5. as const satisfies
export const e = { primary: "red", accent: [0, 128, 255] } as const satisfies Theme;
npx tsc --strict --target es2022 --declaration --emitDeclarationOnly --outDir out five.ts
出力された out/five.d.ts(実行結果)です。
export declare const z: {
primary: string;
accent: number[];
};
export declare const a: Theme;
export declare const b: Theme;
export declare const c: {
readonly primary: "red";
readonly accent: readonly [0, 128, 255];
};
export declare const d: {
primary: "red";
accent: [number, number, number];
};
export declare const e: {
readonly primary: "red";
readonly accent: readonly [0, 128, 255];
};
推論される型の比較表
| パターン | primary の型 |
accent の型 |
キー名は残るか |
|---|---|---|---|
| 0. 何も付けない | string |
number[] |
残る |
| 1. 型注釈 | Color | readonly [number, number, number] |
同左 | 残らない(string なら何でも可) |
| 2. as | Color | readonly [number, number, number] |
同左 | 残らない |
| 3. as const | "red"(readonly) |
readonly [0, 128, 255] |
残る |
| 4. satisfies | "red" |
[number, number, number] |
残る |
| 5. as const satisfies | "red"(readonly) |
readonly [0, 128, 255] |
残る |
読み取れることは3つあります。
- 型注釈と as は、変数の型が
Themeになります。primaryが文字列なのか配列なのか、secondaryというキーがあるのか、といった「値を見ればわかること」が型からは見えなくなります。 - satisfies は検査に使った型(Theme)を手がかりに推論します。 何も付けないと
string/number[]に広がるところが、"red"/[number, number, number]と、Theme に合う形で細かく推論されています。 - as const と as const satisfies は、推論される型が同じです。 違いは「Theme に合っているかの検査があるかどうか」だけです。
使うときに出るエラーの比較表
同じ5つの変数に対して、プロパティの利用・存在しないキーの参照・書き換えを行いました。
a.primary.toUpperCase(); // 1. 型注釈
a.secondary; // 存在しないキー
b.primary.toUpperCase(); // 2. as
c.primary.toUpperCase(); // 3. as const
c.secondary;
d.primary.toUpperCase(); // 4. satisfies
d.secondary;
d.primary = "green"; // 書き換え
d.accent.push(1);
e.primary = "green"; // 5. as const satisfies
e.accent.push(1);
c.primary = "green";
| 操作 | 型注釈 | as | as const | satisfies | as const satisfies |
|---|---|---|---|---|---|
.primary.toUpperCase() |
TS2339 | TS2339 | 通る | 通る | 通る |
.secondary(ないキー) |
通る | 通る | TS2339 | TS2339 | TS2339 |
.primary = "green" |
通る | 通る | TS2540 | TS2322 | TS2540 |
.accent.push(1) |
TS2339(union のため) | TS2339 | TS2339 | 通る | TS2339 |
実際のエラー文(抜粋)です。
five_use.ts(9,11): error TS2339: Property 'toUpperCase' does not exist on type 'readonly [number, number, number] | Color'.
Property 'toUpperCase' does not exist on type 'readonly [number, number, number]'.
five_use.ts(13,3): error TS2339: Property 'secondary' does not exist on type '{ readonly primary: "red"; readonly accent: readonly [0, 128, 255]; }'.
five_use.ts(15,3): error TS2339: Property 'secondary' does not exist on type '{ primary: "red"; accent: [number, number, number]; }'.
five_use.ts(16,1): error TS2322: Type '"green"' is not assignable to type '"red"'.
five_use.ts(18,3): error TS2540: Cannot assign to 'primary' because it is a read-only property.
five_use.ts(19,10): error TS2339: Property 'push' does not exist on type 'readonly [0, 128, 255]'.
型注釈・as では、存在しない a.secondary がエラーになりません。 Record<string, ...> という型は「どんなキーでも持てる」ことを意味するためです。キー名の打ちまちがいを見つけたいなら、キーが残る satisfies か as const satisfies が合います。
もう1点、satisfies だけの d.primary = "green" は TS2322 になりました。 satisfies は Theme を手がかりに primary を "red" と細かく推論するため、書き換え可能でも "red" 以外は入りません。あとで別の色を入れたいプロパティは、次のように型を広げておきます。
const d2 = {
primary: "red" as Color, // このプロパティだけ Color に広げる
accent: [0, 128, 255],
} satisfies Theme;
d2.primary = "green"; // 通る
値をまちがえたときに出るエラーの比較表
次は、定義の時点で値をまちがえた場合です。"red" を "rde" と打ちまちがえました。
const a: Theme = { primary: "rde", accent: [0, 128, 255] };
const b = { primary: "rde", accent: [0, 128, 255] } as Theme;
const c = { primary: "rde", accent: [0, 128, 255] } as const;
const d = { primary: "rde", accent: [0, 128, 255] } satisfies Theme;
const e = { primary: "rde", accent: [0, 128, 255] } as const satisfies Theme;
| パターン | 結果 | エラーコード | エラーが指す位置 |
|---|---|---|---|
| 型注釈 | エラー | TS2322 | primary の値 |
| as | エラー(この例の場合) | TS2352 | オブジェクト全体 |
| as const | 通る("rde" 型として確定する) |
なし | なし |
| satisfies | エラー | TS2322 | primary の値 |
| as const satisfies | エラー | TS2322 | primary の値 |
five_err.ts(5,20): error TS2322: Type '"rde"' is not assignable to type 'readonly [number, number, number] | Color'.
five_err.ts(6,11): error TS2352: Conversion of type '{ primary: "rde"; accent: [number, number, number]; }' to type 'Theme' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Property 'primary' is incompatible with index signature.
Type '"rde"' is not comparable to type 'readonly [number, number, number] | Color'.
five_err.ts(8,13): error TS2322: Type '"rde"' is not assignable to type 'readonly [number, number, number] | Color'.
five_err.ts(9,13): error TS2322: Type '"rde"' is not assignable to type 'readonly [number, number, number] | Color'.
as const だけは型を指定していないので、打ちまちがいも「そういう値」としてそのまま型になります。 as const に satisfies Theme を足すと、型注釈と同じ TS2322 が値の位置に出るようになります。これが2つを組み合わせる理由です。
asが検査をすり抜ける例
上の例では as もエラーを出しましたが、as の検査は「2つの型にまったく重なりがないか」を見るだけです。プロパティが足りない場合や余分な場合は、エラーにならずに通ります。
// as_escape.ts
type User = { id: number; name: string; email: string };
// 1. プロパティが足りなくても通る
const u1 = { id: 1 } as User;
console.log(u1.email.toUpperCase()); // 型の上では string、実行時は undefined
// 2. satisfies なら止まる
const u2 = { id: 1 } satisfies User;
// 3. 型注釈でも止まる
const u3: User = { id: 1 };
// 4. as でも、重なりのない型は止まる
const n = "abc" as number;
// 5. unknown を経由すると何でも通る
const n2 = "abc" as unknown as number;
// 6. 余分なプロパティ
const u4 = { id: 1, name: "a", email: "e", age: 3 } as User;
const u5 = { id: 1, name: "a", email: "e", age: 3 } satisfies User;
実行結果(npx tsc --strict --noEmit --pretty false as_escape.ts)です。
as_escape.ts(8,22): error TS2739: Type '{ id: number; }' is missing the following properties from type 'User': name, email
as_escape.ts(11,7): error TS2739: Type '{ id: number; }' is missing the following properties from type 'User': name, email
as_escape.ts(14,11): error TS2352: Conversion of type 'string' to type 'number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
as_escape.ts(21,44): error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'User'.
| 行 | 書き方 | 結果 |
|---|---|---|
| 4行目 | { id: 1 } as User |
通る(name・email がなくても User 扱い) |
| 8行目 | { id: 1 } satisfies User |
TS2739 |
| 11行目 | const u3: User = { id: 1 } |
TS2739 |
| 14行目 | "abc" as number |
TS2352 |
| 17行目 | "abc" as unknown as number |
通る |
| 20行目 | 余分な age を as User |
通る |
| 21行目 | 余分な age を satisfies User |
TS2353 |
4行目が通ると、5行目の u1.email.toUpperCase() は型の上では問題なしと判定され、実行して初めて undefined のメソッド呼び出しになります。オブジェクトリテラルに型を付ける目的で as T を書いている箇所は、satisfies T か型注釈に置き換えると、足りないプロパティと余分なプロパティの両方をコンパイル時に見つけられます。
as が合うのは、JSON.parse の戻り値や DOM 要素など、コンパイラが型を知りようがない値に、利用者の責任で型を与える場面です。その場合も、型ガード関数で実際に中身を確かめる方法と組み合わせると、型と実際の値がそろいます(絞り込みの書き方は型ガードの記事で扱っています)。
as と as const は名前が似ているが別の機能
| 項目 | as T |
as const |
|---|---|---|
| 正式な呼び方 | 型アサーション | const アサーション |
| すること | 型を T に上書きする | リテラルを最も細かい型で推論させる |
| 型と値がずれる可能性 | ある | ない(値から型を作るため) |
| 書ける対象 | 式なら何でも | リテラル(文字列・数値・真偽値・配列・オブジェクト)と enum メンバー |
as const は値をもとに型を決めるので、型と実際の値がずれることがありません。as という綴りは共通ですが、性質は反対です。
as constの使い方と推論のルール
as const を付けると、次の3つが同時に起こります。
- 文字列・数値・真偽値がリテラル型になる(
"red"はstringではなく"red") - オブジェクトのプロパティがすべて readonly になる
- 配列が readonly のタプルになる(
readonly [0, 128, 255])
この3つは入れ子の奥まで適用されます。
// ro.ts(抜粋)
export const deep = { list: [1, 2], nested: { a: { b: "x" } } } as const;
export let s1 = "todo" as const;
export let s2 = "todo";
export const s3 = "todo";
export const t1 = [1, "a"] as const;
export const t2 = [1, "a"];
.d.ts の出力(実行結果)です。
export declare const deep: {
readonly list: readonly [1, 2];
readonly nested: {
readonly a: {
readonly b: "x";
};
};
};
export declare let s1: "todo";
export declare let s2: string;
export declare const s3 = "todo";
export declare const t1: readonly [1, "a"];
export declare const t2: (string | number)[];
| 書き方 | 推論される型 |
|---|---|
let s2 = "todo" |
string |
let s1 = "todo" as const |
"todo" |
const s3 = "todo" |
"todo"(const 宣言なら as const なしでリテラル型) |
const t2 = [1, "a"] |
(string | number)[] |
const t1 = [1, "a"] as const |
readonly [1, "a"] |
const で宣言しても、オブジェクトや配列の中身は書き換えられるため、中の型は string や number[] に広がります。 中身まで固定したいときに as const を使います。
関数に渡すと string に広がってしまうとき
次のエラーは、as const を知るきっかけになりやすいものです。
type Dir = "up" | "down";
function move(d: Dir) {}
const opt = { dir: "up" };
move(opt.dir); // エラー
const opt2 = { dir: "up" } as const;
move(opt2.dir); // 通る
const opt3 = { dir: "up" } satisfies { dir: Dir };
move(opt3.dir); // 通る
misc_err.ts(8,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Dir'.
opt.dir は string と推論されるため、"up" | "down" を求める引数に渡せません。as const を付けるか、satisfies { dir: Dir } で検査すると、dir が "up" と推論されて通ります。 なお let opt4 = { dir: "up" } satisfies { dir: Dir } と let で宣言した場合も、同じく通ることを確認しました。
as constを書ける場所・書けない場所
as const はリテラルにだけ付けられます。変数や関数の戻り値に付けると TS1355 になります。
const v = Math.random() as const; // 関数の戻り値
let n = [1, 2, 3];
const m = n as const; // 変数
const p = { a: 1 } satisfies { a: number } as const; // 順序が逆
misc_err.ts(1,11): error TS1355: A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals.
misc_err.ts(3,11): error TS1355: A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals.
misc_err.ts(4,20): error TS1355: A 'const' assertion can only be applied to references to enum members, or string, number, boolean, array, or object literals.
順序は必ず as const satisfies T です。 satisfies T as const と書くと、satisfies の結果はリテラルではないため TS1355 になります。
変数を経由した値の扱いも確認しました。
const input = "todo";
export const o1 = { status: input }; // { status: string }
export const o2 = { status: input } as const; // { readonly status: "todo" }
declare const x: string;
export const o3 = { v: x } as const; // { readonly v: string }
as const は「広げない」という指示なので、もともと string 型の変数 x を入れた場合は string のままです。リテラル型になるのは、リテラルを直接書いたところと、すでにリテラル型を持つ const 変数を入れたところです。
readonlyとの関係
as const の readonly と、Readonly<T>、Object.freeze は似ていますが、届く深さと実行時の動きが違います。
export const deep = { list: [1, 2], nested: { a: { b: "x" } } } as const;
export const ro: Readonly<{ list: number[]; nested: { a: { b: string } } }> =
{ list: [1, 2], nested: { a: { b: "x" } } };
ro.nested.a.b = "y"; // 通る(Readonly は1階層)
deep.nested.a.b = "y"; // TS2540(as const は全階層)
ro.list.push(3); // 通る
ro.ts(18,15): error TS2540: Cannot assign to 'b' because it is a read-only property.
| 方法 | 型の上の readonly | 届く深さ | リテラル型になるか | 実行時に書き換えを止めるか |
|---|---|---|---|---|
as const |
あり | すべての階層 | なる | 止めない |
Readonly<T> |
あり | 1階層 | ならない | 止めない |
readonly 修飾子(型定義に書く) |
あり | 書いたプロパティだけ | ならない | 止めない |
Object.freeze(値) |
あり(Readonly<T> が付く) |
1階層 | 文字列などは細かくなる | 止める(1階層) |
as constは実行時には何もしない
as const は型の世界だけの指示で、出力される JavaScript からは消えます。実際に確かめました。
// rt.ts
const cfg = { port: 3000, tags: ["a"] } as const;
(cfg as any).port = 8080;
(cfg.tags as unknown as string[]).push("b");
console.log(cfg);
const fz = Object.freeze({ port: 3000, tags: ["a"] });
try { (fz as any).port = 8080; } catch (e) { console.log((e as Error).message); }
(fz.tags as string[]).push("b");
console.log(fz);
実行結果(Node.js 22.22.2)です。
{ port: 8080, tags: [ 'a', 'b' ] }
Cannot assign to read only property 'port' of object '#<Object>'
{ port: 3000, tags: [ 'a', 'b' ] }
as const のオブジェクトは、型を外せば書き換えられます。実行時にも書き換えを止めたい定数は Object.freeze を併用します。 ただし Object.freeze も1階層だけなので、tags 配列への push は通っています。
readonly配列を関数に渡すと出るTS4104
as const を付けた配列を、number[] を受け取る関数に渡すと次のエラーになります。
function f(xs: number[]) {}
f(deep.list);
ro.ts(16,3): error TS4104: The type 'readonly [1, 2]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
number[] を受け取る関数は中身を書き換えるかもしれないので、readonly な配列は渡せない、という判定です。関数の中で配列を書き換えないなら、引数の型を readonly number[] にすると、通常の配列も as const の配列も両方受け取れます。
function f(xs: readonly number[]) {} // どちらも渡せる
f(deep.list);
f([1, 2, 3]);
satisfiesの使い方と型注釈との違い
satisfies は「この式は型 T に合っていますか」とコンパイラに確認するだけの演算子です。合っていれば何も変えず、合わなければエラーにします。
| 比べる点 | 型注釈 const x: T = 値 |
値 satisfies T |
|---|---|---|
| 変数の型 | T | 値から推論した型 |
| 余分なプロパティ | エラー(TS2353) | エラー(TS2353) |
| 足りないプロパティ | エラー(TS2739 など) | エラー(TS2739 など) |
| union のどれかが確定するか | 確定しない | 確定する |
| キーの補完 | T に書いたキーだけ | 実際に書いたキー |
| 書ける場所 | 変数宣言・引数・戻り値 | 式ならどこでも |
「式ならどこでも書ける」点は意外と便利です。変数を作らずに、その場で検査できます。
type Task = { title: string; done: boolean };
// 戻り値を検査しつつ、細かい型で返す
export function createTask(title: string) {
return { title, done: false, createdAt: Date.now() } satisfies Task & { createdAt: number };
}
// default export の検査
export default { title: "初期タスク", done: false } satisfies Task;
// JSON.stringify に渡す直前の検査
const body = JSON.stringify({ title: "買い物", done: false } satisfies Task);
配列でも同じように使えます。
type Dir = "up" | "down";
const xs = ["up", "down"] satisfies Dir[]; // 型は ("up" | "down")[]
const first: Dir = xs[0]; // 通る
satisfies Dir[] を付けないと xs は string[] になり、Dir には代入できません。
コピペで使える定義パターン集
ここからは、そのまま貼って使える定義です。すべて1つのファイル(recipes.ts)にまとめてコンパイル・実行しました。
設定オブジェクトを定義する
type AppConfig = {
env: "dev" | "stg" | "prod";
port: number;
features: Record<string, boolean>;
retry?: { times: number; delayMs: number };
};
export const config = {
env: "dev",
port: 3000,
features: { search: true, export: false },
} as const satisfies AppConfig;
export type FeatureName = keyof typeof config.features; // "search" | "export"
推論された型(.d.ts の出力)です。
export declare const config: {
readonly env: "dev";
readonly port: 3000;
readonly features: {
readonly search: true;
readonly export: false;
};
};
features は Record<string, boolean> で検査されていますが、search と export というキー名がそのまま残っているので、FeatureName という union 型を取り出せます。型注釈で const config: AppConfig = ... と書いた場合、keyof typeof config.features は string になります。
ルート定義を作る
type RouteDef = { path: `/${string}`; title: string; auth?: boolean };
export const routes = {
home: { path: "/", title: "ホーム" },
tasks: { path: "/tasks", title: "タスク一覧", auth: true },
taskDetail: { path: "/tasks/:id", title: "タスク詳細", auth: true },
} as const satisfies Record<string, RouteDef>;
export type RouteName = keyof typeof routes; // "home" | "tasks" | "taskDetail"
export type RoutePath = (typeof routes)[RouteName]["path"]; // "/" | "/tasks" | "/tasks/:id"
export function href(name: RouteName): RoutePath {
return routes[name].path;
}
path の型を `/${string}`(スラッシュで始まる文字列)にしておくと、先頭のスラッシュを書き忘れた定義を検査で見つけられます。まちがえた定義を書いてみます。
href("task"); // 存在しないルート名
const bad = {
home: { path: "home", title: "ホーム" }, // 先頭に / がない
about: { path: "/about", titel: "概要" }, // キーのつづり
} as const satisfies Record<string, RouteDef>;
recipes_err.ts(3,6): error TS2345: Argument of type '"task"' is not assignable to parameter of type '"home" | "taskDetail" | "tasks"'.
recipes_err.ts(7,11): error TS2322: Type '"home"' is not assignable to type '`/${string}`'.
recipes_err.ts(8,28): error TS2561: Object literal may only specify known properties, but 'titel' does not exist in type 'RouteDef'. Did you mean to write 'title'?
キーのつづりちがいには「Did you mean to write 'title'?」と候補まで出ます。 as const だけの定義では、この3つはどれも検出されません。
enumの代わりに定数オブジェクトを使う
export const Status = { Todo: "todo", Doing: "doing", Done: "done" } as const;
export type Status = (typeof Status)[keyof typeof Status]; // "todo" | "doing" | "done"
export function label(s: Status): string {
switch (s) {
case Status.Todo: return "未着手";
case Status.Doing: return "進行中";
case Status.Done: return "完了";
}
}
(typeof Status)[keyof typeof Status] は、次の順に読むとわかりやすくなります。
| 式 | 意味 | 結果 |
|---|---|---|
typeof Status |
定数オブジェクトの型 | { readonly Todo: "todo"; readonly Doing: "doing"; readonly Done: "done" } |
keyof typeof Status |
そのキーの union | "Todo" | "Doing" | "Done" |
(typeof Status)[keyof typeof Status] |
全キーで値の型を取り出す | "todo" | "doing" | "done" |
定数と型に同じ名前 Status を付けられるのは、TypeScript では値と型の名前空間が別だからです。使う側は Status.Todo(値)と s: Status(型)を enum と同じ感覚で書けます。存在しない値を渡すと次のエラーになります。
recipes_err.ts(4,7): error TS2345: Argument of type '"finished"' is not assignable to parameter of type 'Status'.
ここで as const を外すと、Status 型は string になり、どんな文字列でも通るようになります。 このパターンは as const があって初めて成り立ちます。enum と union 型のどちらを選ぶかの判断基準は、enum と union 型を比べた記事で詳しく扱っています。
配列からunion型を作る
export const SIZES = ["S", "M", "L", "XL"] as const;
export type Size = (typeof SIZES)[number]; // "S" | "M" | "L" | "XL"
export function isSize(v: string): v is Size {
return (SIZES as readonly string[]).includes(v);
}
(typeof SIZES)[number] は「配列のどれかの要素の型」を意味します。配列を1つ書くだけで、画面の選択肢(値)と型の両方に使えるので、片方だけ直し忘れることがなくなります。
isSize の中で SIZES as readonly string[] としているのは、SIZES.includes(v) に string を直接渡すと次のエラーになるためです。
v.ts(16,16): error TS2345: Argument of type 'string' is not assignable to parameter of type '"L" | "M" | "S" | "XL"'.
ここは型を広げる方向の as なので、値と型がずれることはありません。
オブジェクトの配列からキーのunion型を作る
export const COLUMNS = [
{ key: "id", label: "ID", width: 60 },
{ key: "title", label: "件名", width: 240 },
{ key: "status", label: "状態", width: 100 },
] as const satisfies readonly { key: string; label: string; width: number }[];
export type ColumnKey = (typeof COLUMNS)[number]["key"]; // "id" | "title" | "status"
as const を付けた配列は readonly になるので、satisfies の右側も readonly ...[] とそろえて書いています。 TypeScript 7.0.2 では readonly を付けない { ... }[] でも通ることを確認しましたが、そろえておくと「書き換えない配列」という意図が読み手に伝わります。
実行して確かめる
console.log(href("taskDetail"), label("doing"), isSize("M"), isSize("XXL"));
console.log(Object.values(Status), COLUMNS.map((c) => c.key));
npx tsc --strict --target es2022 --module nodenext --outDir js recipes.ts
node js/recipes.js
/tasks/:id 進行中 true false
[ 'todo', 'doing', 'done' ] [ 'id', 'title', 'status' ]
出力された JavaScript には as const も satisfies も残りません。
exports.Status = { Todo: "todo", Doing: "doing", Done: "done" };
exports.SIZES = ["S", "M", "L", "XL"];
どちらもコンパイル時だけの機能なので、実行速度やファイルサイズには影響しません。
const型パラメータで呼び出し側のas constを省く
TypeScript 5.0 で、型パラメータに const を付けられるようになりました。関数の側で「引数を as const と同じように推論する」と宣言できます。
// constparam.ts
export function tuple1<T extends readonly unknown[]>(xs: T): T { return xs; }
export function tuple2<const T extends readonly unknown[]>(xs: T): T { return xs; }
export const r1 = tuple1(["a", "b"]);
export const r2 = tuple2(["a", "b"]);
export const r3 = tuple1(["a", "b"] as const);
export function defineRoutes<const T extends Record<string, { path: string }>>(r: T): T {
return r;
}
export const rt = defineRoutes({ home: { path: "/" }, tasks: { path: "/tasks" } });
.d.ts の出力(実行結果)です。
export declare const r1: string[];
export declare const r2: readonly ["a", "b"];
export declare const r3: readonly ["a", "b"];
export declare const rt: {
readonly home: {
readonly path: "/";
};
readonly tasks: {
readonly path: "/tasks";
};
};
| 呼び出し | 推論される型 |
|---|---|
tuple1(["a", "b"]) |
string[] |
tuple1(["a", "b"] as const) |
readonly ["a", "b"] |
tuple2(["a", "b"])(const 型パラメータ) |
readonly ["a", "b"] |
defineRoutes({...}) のように、定義用の関数を1つ用意しておくと、使う側は as const を書かなくても細かい型が得られます。 しかも T extends Record<string, { path: string }> という制約が satisfies と同じ検査の役目をします。
const型パラメータの2つの注意点
// readonly を付けない制約
export function tuple3<const T extends unknown[]>(xs: T): T { return xs; }
export const r4 = tuple3(["a", "b"]);
// 変数を経由して渡す
const arr = ["a", "b"];
export const r5 = tuple2(arr);
export declare const r4: ["a", "b"];
export declare const r5: string[];
| 書き方 | 結果 | 理由 |
|---|---|---|
制約が readonly unknown[] |
readonly ["a", "b"] |
as const と同じ推論になる |
制約が unknown[](readonly なし) |
["a", "b"] |
可変の配列型に合わせるため、readonly が外れたタプルになる |
変数 arr を渡す |
string[] |
const の効果は、呼び出しの場所に直接書いたリテラルにだけ及ぶ |
readonly を付けない制約の結果は、バージョンによって変わります。同じ tuple3(["a", "b"]) を3つのバージョンでコンパイルして比べました。
| TypeScript | r4 の型 |
|---|---|
| 5.0.4 | unknown[] |
| 5.2.2 | unknown[] |
| 5.3.3 | ["a", "b"] |
| 7.0.2 | ["a", "b"] |
5.2 までは「readonly のタプルは可変の配列型に合わない」ため制約の型そのものに広がり、5.3 からは readonly を外したタプルとして推論されます。配列を受け取る const 型パラメータの制約には readonly を付けておくと、どのバージョンでも同じ結果になります。
どれを使うかの判断表
flowchart TD
A[オブジェクトや配列を定義する] --> B{あとで書き換えるか}
B -- 書き換えない --> C{合わせたい型があるか}
C -- ある --> D[as const satisfies T]
C -- ない --> E[as const]
B -- 書き換える --> F{キー名や値の型を残したいか}
F -- 残したい --> G[satisfies T]
F -- T そのものでよい --> H[型注釈]
A --> I{コンパイラが型を知らない値か}
I -- はい --> J[as T と型ガードを組み合わせる]
| やりたいこと | 書き方 |
|---|---|
| 定数の一覧から union 型を作りたい | [...] as const と (typeof X)[number] |
| enum の代わりの定数を作りたい | {...} as const と (typeof X)[keyof typeof X] |
| 設定やルートを、型を検査しながら定義したい | as const satisfies T |
| キーの打ちまちがいを見つけたい | satisfies Record<キーの型, 値の型> |
| union 型のプロパティを、絞り込みなしで使いたい | satisfies T(型注釈だと union のまま) |
| 状態を持つ変数で、あとで別の値を入れる | 型注釈(let s: Status = "todo") |
| 関数の引数・戻り値の型を決める | 型注釈 |
| API のレスポンスなど外から来た値 | 型ガードで確かめる(やむを得ないときに as T) |
| ライブラリの定義関数を作る | const 型パラメータ |
よく出るエラーと直し方
| エラー | 出る場面 | 直し方 |
|---|---|---|
TS2345 Argument of type 'string' is not assignable to parameter of type ... |
オブジェクトのプロパティが string に広がった |
定義に as const か satisfies T を付ける |
TS2540 Cannot assign to 'x' because it is a read-only property. |
as const の値を書き換えた | 書き換える値なら satisfies T だけにする |
TS4104 The type 'readonly [...]' is 'readonly' and cannot be assigned to the mutable type |
readonly 配列を可変配列の引数に渡した | 引数の型を readonly T[] にする |
TS1355 A 'const' assertion can only be applied to ... |
変数や式に as const を付けた、順序が逆 | リテラルに直接付ける。順序は as const satisfies T |
TS2322 Type '"green"' is not assignable to type '"red"'. |
satisfies で細かく推論された値を書き換えた | そのプロパティを "red" as Color と広げる |
TS2353 / TS2561 Object literal may only specify known properties |
satisfies が余分なキーを検出した | キー名を直す(意図したキーなら型定義に足す) |
エラーコードごとの読み方は、TypeScript のエラー一覧の記事にまとめています。
よくある質問
Q. as const と satisfies はどちらを使えばよいですか?
A. 役割が別なので、選ぶのではなく組み合わせます。値を細かい型で固定するのが as const、型に合っているかを検査するのが satisfies です。定数の定義には as const satisfies T と両方書くと、推論される型は as const と同じまま、値のまちがいを TS2322 などで見つけられます。
Q. as const satisfies T と satisfies T as const はどちらが正しいですか?
A. as const satisfies T です。逆順にすると、TypeScript 7.0.2 では TS1355(const アサーションはリテラルにだけ付けられる)になります。「まず as const で型を固定し、その結果を satisfies で検査する」という順番と覚えると書きやすくなります。
Q. satisfies を使えば型注釈は書かなくてよいですか?
A. 場面によって使い分けます。関数の引数・戻り値や、あとで別の値を入れる変数は、型注釈で「この型である」と決めるのが合います。satisfies が合うのは、オブジェクトリテラルや配列リテラルを定義する場面で、キー名や値の細かい型を残したいときです。
Q. as const を付けると実行時にも書き換えられなくなりますか?
A. なりません。as const はコンパイル時だけの指示で、出力される JavaScript には残りません。実行時にも止めたい場合は Object.freeze を併用します。Object.freeze は1階層だけなので、入れ子の奥まで止めるには階層ごとに適用します。
まとめ
- as const は「リテラル型+すべての階層を readonly」で推論させる書き方、satisfies は「型はそのまま、合っているかだけ検査する」演算子
- 型注釈と as は変数の型が T に広がり、キー名や union のどれかという情報が残らない。 satisfies と as const は値から推論した型が残る
as Tは不足プロパティ・余分なプロパティが通る。 オブジェクトリテラルに型を付ける目的ならsatisfies Tか型注釈にすると TS2739 / TS2353 で見つかる- 定数の定義は
as const satisfies Tが基本形。 順序を逆にすると TS1355 になる - enum の代替は
(typeof X)[keyof typeof X]、配列から union 型は(typeof X)[number]。 どちらも as const が前提 - const 型パラメータ(5.0〜)を使うと、呼び出し側が as const を書かなくても細かい型になる。 配列の制約には readonly を付ける
- as const は実行時には何もしない。 実行時にも固定したい値は Object.freeze を併用する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScriptのas constとsatisfiesの違いを比較表で整理」
株式会社RJCでは、TypeScriptの型の書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。