Menu

TypeScript satisfies 运算符:与类型注解和 as 的区别

satisfies 运算符检查一个值是否符合某个类型,同时不改变这个值的推断类型。本文讲解它的作用、它与类型注解和 as 的区别(同一个对象的三种写法)、如何与 as const 结合,以及它为什么适合配置对象。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

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 Tv satisfies T
缺失的属性报错允许报错
多余的属性(对象字面量)报错允许报错
属性类型错误报错仅当类型毫无重叠时报错报错
之后 x 的类型TTv 的推断类型
字面量类型("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)都支持它。

Coddy 编程语言插图

和 Coddy 一起学编程

开始