satisfies(TS 4.9+)解决了一个长期痛点:注解会丢失推断,不注解会丢失检查。它让你同时得到两者。
注解的两个问题
1type Route = { path: string; method: "GET" | "POST" };
2
3// 问题一:注解丢失推断
4const routes1: Route[] = [{ path: "/a", method: "GET" }];
5// routes1[0].method 的类型是 "GET" | "POST",丢了具体值
6
7// 问题二:不注解丢失检查
8const routes2 = [{ path: "/a", method: "GETT" }]; // 拼错了也不报错
satisfies 的语义是:检查这个值满足某个类型,但不要用那个类型去覆盖推断出来的类型。
1const routes3 = [
2 { path: "/a", method: "GET" },
3 { path: "/b", method: "POST" },
4] satisfies Route[]; // 有检查,且保留了字面量类型
四种写法的对比
1// satisfies:既检查又保留推断
2
3type Route = { path: string; method: "GET" | "POST" };
4
5// ---- 问题:注解丢失推断 ----
6const routes1: Route[] = [{ path: "/a", method: "GET" }];
7// routes1[0].method 的类型是 "GET" | "POST",丢了具体值
8
9// ---- 问题:不注解丢失检查 ----
10const routes2 = [{ path: "/a", method: "GETT" }]; // 拼错了也不报错
11
12// ---- satisfies 同时解决两者 ----
13const routes3 = [
14 { path: "/a", method: "GET" },
15 { path: "/b", method: "POST" },
16] satisfies Route[];
17
18// 数组 satisfies 后,索引访问得到元素类型的联合
19type Route3Method = (typeof routes3)[number]["method"]; // "GET" | "POST"
20
21// 单个对象才保留具体字面量
22const single = { path: "/a", method: "GET" } satisfies Route;
23const r3: "GET" = single.method; // ✓ 保留字面量 "GET"
24
25// 数组要保留每个位置的字面量,用 as const satisfies
26const tuple = [
27 { path: "/a", method: "GET" },
28 { path: "/b", method: "POST" },
29] as const satisfies readonly Route[];
30const t0: "GET" = tuple[0].method; // ✓ 元组保留位置信息
31
32// ---- 检查对象键完整 ----
33type Env = "dev" | "prod";
34const urls = {
35 dev: "http://localhost",
36 prod: "https://example.com",
37} satisfies Record<Env, string>;
38// 少写一个键会报错
39
40// satisfies 保留具体键
41type K = keyof typeof urls; // "dev" | "prod"
42// 对比:注解会退化成 Env
43const urls2: Record<Env, string> = { dev: "a", prod: "b" };
44type K2 = keyof typeof urls2; // Env
45
46// ---- 检查值符合约束 ----
47type Color = "red" | "green" | "blue";
48const palette = {
49 primary: "red",
50 secondary: "green",
51} satisfies Record<string, Color>;
52
53// ---- 对比四种写法的差异 ----
54// 1. 注解:有检查,丢推断
55const a1: Route = { path: "/a", method: "GET" };
56type T1 = typeof a1.method; // "GET" | "POST"
57
58// 2. 无注解:有推断,无检查
59const a2 = { path: "/a", method: "GET" as const };
60type T2 = typeof a2.method; // "GET"
61
62// 3. satisfies:有检查 + 有推断
63const a3 = { path: "/a", method: "GET" } satisfies Route;
64type T3 = typeof a3.method; // "GET"
65
66// 4. as const satisfies:最严格
67const a4 = { path: "/a", method: "GET" } as const satisfies Route;
68type T4 = typeof a4.method; // "GET",且只读
69
70const methodUnion: Route3Method = "GET";
71console.log(routes1, routes2, routes3, single, tuple, urls, urls2, palette, a1, a2, a3, a4, methodUnion, r3, t0);
1[ { path: '/a', method: 'GET' } ] [ { path: '/a', method: 'GETT' } ] [ { path: '/a', method: 'GET' }, { path: '/b', method: 'POST' } ] { path: '/a', method: 'GET' } [ { path: '/a', method: 'GET' }, { path: '/b', method: 'POST' } ] { dev: 'http://localhost', prod: 'https://example.com' } { dev: 'a', prod: 'b' } { primary: 'red', secondary: 'green' } { path: '/a', method: 'GET' } { path: '/a', method: 'GET' } { path: '/a', method: 'GET' } { path: '/a', method: 'GET' } GET GET GET
| 写法 | 有检查 | 保留字面量 | method 的类型 |
|---|---|---|---|
const a: Route = {...} | ✓ | ✗ | "GET" | "POST" |
const a = {...} | ✗ | ✓ | string |
const a = {...} satisfies Route | ✓ | ✓ | "GET" |
... as const satisfies Route | ✓ | ✓ + 只读 | "GET" |
satisfies 的读法把 x satisfies T 读作"x 必须满足 T,但 x 还是它自己"。注解 x: T 读作"x 就是 T"——后者会丢弃 x 更具体的信息。
satisfies 的一个细节:数组 vs 单对象
数组用 satisfies 后,索引访问得到的是元素类型的联合,不是每个位置的具体类型:
1const routes3 = [...] satisfies Route[];
2
3type Route3Method = (typeof routes3)[number]["method"]; // "GET" | "POST"
4// 不是 "GET",因为数组的每个元素类型被统一了
单个对象才能保留具体字面量:
1const single = { path: "/a", method: "GET" } satisfies Route;
2const r3: "GET" = single.method; // ✓
数组要保留每个位置的信息,必须加 as const(让数组变成元组):
1const tuple = [
2 { path: "/a", method: "GET" },
3 { path: "/b", method: "POST" },
4] as const satisfies readonly Route[];
5
6const t0: "GET" = tuple[0].method; // ✓ 元组保留了位置
检查对象键完整
这是 satisfies 最实用的场景之一:
1type Env = "dev" | "prod";
2
3const urls = {
4 dev: "http://localhost",
5 prod: "https://example.com",
6} satisfies Record<Env, string>;
7// 少写一个键会报错
而且它保留具体的键,不像注解那样退化成 Env:
1type K = keyof typeof urls; // "dev" | "prod"(satisfies)
2
3const urls2: Record<Env, string> = { dev: "a", prod: "b" };
4type K2 = keyof typeof urls2; // Env(注解丢失了具体键)
检查值符合约束
1type Color = "red" | "green" | "blue";
2
3const palette = {
4 primary: "red",
5 secondary: "green",
6} satisfies Record<string, Color>; // 值必须是 Color 之一
as const 的补充
satisfies 只做检查,不改只读性。要同时获得只读和字面量,用 as const satisfies:
1const config = {
2 mode: "dark",
3 tags: ["a", "b"],
4} as const satisfies { mode: string; tags: readonly string[] };
5
6config.mode; // "dark",且只读
7config.tags[0]; // "a"
注意 satisfies 的类型里要用 readonly,因为 as const 把数组变成了只读元组。详见 只读与不可变。
选择指南
| 想要 | 写 |
|---|---|
| 值必须符合类型,且丢弃具体信息 | const x: T = ... |
| 只想要检查,不想改变类型 | const x = ... satisfies T |
| 检查 + 只读 + 字面量 | const x = ... as const satisfies T |
| 只想要推断,不要检查 | const x = ... |