value satisfies Type 会在编译时检查 value 是否符合 Type,然后保持这个值自身更精确的类型不变。注解会用 Type 替换掉这个精确的类型;satisfies 只做校验,不会拓宽类型。
satisfies 照样会做检查:缺少某种颜色、拼错键名(比如 bleu),或者值写成了 true,都会在那一行报编译错误。它从 TypeScript 4.9 开始提供,和所有类型注解一样,会从生成的 JavaScript 中删除。
satisfies 解决的问题
使用类型注解时,变量的类型就是注解的类型。编译器会忘掉它在字面量中看到的信息。下面把同一个调色板改成用注解,这时 TypeScript 就不再知道 green 是字符串了:
编译器报告:
index.ts(11,27): error TS2339: Property 'toUpperCase' does not exist on type 'Color'.
Property 'toUpperCase' does not exist on type '[number, number, number]'.
在 TypeScript 4.9 之前,只能二选一:要么加注解,然后处处手动收窄类型(typeof palette.green === "string");要么不加注解,失去检查。satisfies 让你两者兼得。把 : Record<ColorName, Color> 改成写在右花括号后面的 satisfies Record<ColorName, Color>,代码就能运行了。
satisfies、类型注解与 as 对比
同一个设置对象的三种写法:
as 放过了缺失的 lang,于是 asserted.lang 在运行时是 undefined,而它的类型却声称是 string。从另外两行中去掉 lang,两者都会以 TS2741 失败:Property 'lang' is missing in type ...。
注解 const x: T = v | 断言 v as T | v satisfies T | |
|---|---|---|---|
| 缺失的属性 | 报错 | 允许 | 报错 |
| 多余的属性(对象字面量) | 报错 | 允许 | 报错 |
| 属性类型错误 | 报错 | 仅当类型毫无重叠时报错 | 报错 |
之后 x 的类型 | T | T | v 的推断类型 |
字面量类型("dark"、8080) | 拓宽为 T | 拓宽为 T | 在 T 允许的地方保留 |
Record<string, ...> 的键 | 任意字符串(拼写错误也能编译) | 任意字符串 | 恰好是写出的那些键 |
| 运行时影响 | 无 | 无 | 无 |
经验法则:希望变量拥有声明的类型时(会被重新赋值的值、公共 API),就用注解;想要检查、但值自身的类型更有用时,就用 satisfies。
捕获对象字面量中的错误
satisfies 会执行完整的可赋值性检查,包括多余属性检查,所以键名拼写错误会报错:
type Route = { path: string; method: "GET" | "POST" };
const home = { path: "/", metod: "GET" } satisfies Route;
// error TS2561: Object literal may only specify known properties, but 'metod' does not exist in type 'Route'. Did you mean to write 'method'?
这项检查还会像注解一样,为字面量提供上下文类型。这在两方面很重要。当目标类型需要字面量类型时,字符串字面量会保留为字面量类型:{ path: "/", method: "GET" } satisfies Route 中是 method: "GET",而同一个对象如果不加注解,会被推断为 method: string。另外,回调函数的参数类型也会从目标类型推断出来:
Record 的键保持已知
一个常见用途是查找表。如果注解为 Record<string, T>,任何字符串都是合法的键,拼写错误也能通过编译,并在运行时返回 undefined。使用 satisfies 时,值仍会按 T 检查,但变量的类型恰好列出了你写的那些键:
keyof typeof endpoints 之所以有用,正是因为键被保留了下来。如果用注解,它就只是 string。
如果要求一组固定的键,就让值满足以联合类型为键的 Record:satisfies Record<"dev" | "prod", string> 会以 TS2741 报告缺少 prod,以 TS2353 报告未知的 staging。
as const satisfies
as const 和 satisfies 可以组合使用。as const 要写在前面:它让值变成深层 readonly 并带有字面量类型,然后 satisfies 再检查这个精确的值。
每条路由都会按 Route 检查(写成 method: "PUT" 就会报错),同时字面量类型的元组依然可用,所以 Path 是真实路径组成的联合类型。目标类型要用 readonly Route[](或 ReadonlyArray<Route>),因为 as const 数组是只读的。
配置对象
配置是 satisfies 大显身手的地方:结构必须正确,而其他地方的代码又需要精确的值。
忘了写 production 这一项、拼错 logLevel,或者写成 logLevel: "verbose",编译器都会指出具体是哪一行。同样的模式也适用于 *.config.ts 文件:export default { ... } satisfies SomeConfig 会检查整个文件,同时导出的对象保留它的字面量值。
什么时候不该用 satisfies
- 变量会被重新赋值。
let cfg = { port: 3000 } satisfies { port: number | string }让cfg的类型变成{ port: number },所以之后的cfg = { port: "80" }会失败(TS2322)。打算修改的变量要用注解。 - 你本来就想要声明的类型。 对于函数返回值或者作为 API 一部分导出的常量,注解的类型就是契约,泄露精确的字面量类型可能让以后的修改变成破坏性变更。
- 值不是字面量。
satisfies在对象和数组字面量上最能发挥作用。用在变量或函数调用结果上时,它只是普通的可赋值性检查,而注解本来就能做到这一点。
常见问题
TypeScript 中的 satisfies 有什么作用?
expression satisfies Type 会在编译时检查表达式能否赋值给 Type,报告缺失的属性、多余的属性和类型错误的值,然后保持表达式自身的推断类型不变。你既得到了注解的安全性,又保留了类型推断的精确性。它会从输出的 JavaScript 中被擦除。
satisfies 和类型注解有什么区别?
两者都会检查值。注解(const x: T = ...)随后会把变量的类型设为 T,丢掉编译器对这个值所知道的信息(字面量类型、每个属性属于联合类型的哪个成员、有哪些键)。satisfies T 保留推断类型,所以编译器知道 x.someKey 存在,而一个类型为 string | number、实际存放字符串的属性会被推断为 string。
TypeScript 中 satisfies 和 as 有什么区别?
as 是断言:它覆盖类型,几乎不做检查,所以缺失的属性不会被发现。satisfies 是检查:值必须真正符合类型,并且保留它自己的推断类型。当两者都能通过编译时,satisfies 是更安全的选择。
as const satisfies 是什么意思?
它同时应用两者:as const 让值变成深层只读并带有字面量类型,然后 satisfies 用某个类型检查这个结果。as const 要写在前面:const routes = [...] as const satisfies readonly Route[];。变量保留了精确的字面量类型供后续使用,而错误的条目仍然会导致编译错误。
satisfies 是哪个 TypeScript 版本加入的?
TypeScript 4.9,于 2022 年 11 月发布。它是普通的可擦除语法,所以在 Node 内置的类型剥离下也能运行,当前所有 TypeScript 版本(包括 7)都支持它。