any 和 unknown 都能接受任何值。区别在于之后你能对这个值做什么:any 允许你做任何事,什么都不检查;而 unknown 在你证明这个值是什么之前,几乎什么都不让你做。
去掉 @ts-expect-error 那一行,u.toUpperCase() 就是编译错误 TS18046。typeof 检查把 u 收窄为 string,在那个代码块里,所有字符串方法都可以使用。
any 与 unknown 一览
any | unknown | |
|---|---|---|
| 接受任何值 | 是 | 是 |
赋值给 string、number 等 | 可以,不检查 | 不可以(TS2322) |
| 读取属性、调用方法 | 可以,不检查 | 不可以(TS18046) |
| 当作函数调用 | 可以 | 不可以 |
算术和比较运算(x * 2、x + 1、x < 5) | 可以 | 不可以(TS18046) |
| 使用前需要检查 | 不需要 | 需要(typeof、instanceof、in、类型守卫) |
| 对类型检查的影响 | 对这个值及其涉及的一切关闭检查 | 保持开启 |
用类型论的说法,unknown 是顶层类型(top type):任何类型都可以赋给它,而它只能赋给 unknown 和 any。any 则是一个逃生口,两个方向都能赋值,除了 never 之外可以赋给任何类型。
any 会关闭类型检查
类型为 any 的值会被无条件信任。编译器接受拼写错误、错误的类型和不存在的属性,这些错误要到运行时才会暴露。
输出说明了问题所在:一个标注为 number 的变量里装着字符串,最后一行抛出 Cannot read properties of undefined (reading 'city')。any 还会扩散。user.name 是 any,所以由它计算出的任何值也都是 any,一个没有类型的值可以在远离它进入代码的地方关闭类型检查。
unknown 要求你先检查
对于 unknown,在代码收窄这个值之前,编译器拒绝一切操作。收窄用的是普通的 JavaScript 检查,在每个分支里,值都具有被检查过的类型。
value === null 检查必须放在对象检查之前,因为 typeof null 是 "object"。经过 "id" in value 之后,TypeScript 知道这个对象有一个 id 属性,类型为 unknown,因为没有任何信息说明它装的是什么。String() 把它显式转换成文本;如果要把它当数字用,你得先用 typeof 检查它。
你也可以用断言 value as string 跳过检查,编译器会接受。但这只是一个承诺,背后没有任何运行时检查,所以最好使用真正的检查;参见类型断言。
用 unknown 校验 JSON
JSON.parse 被声明为返回 any,所以它的结果会悄悄关闭类型检查。把结果标注为 unknown,再写一个类型守卫,在其余代码信任它之前先检查它的结构。
第一个输入打印 dark at 14px,第二个因为缺少 fontSize 而被拒绝。如果用 any,第二个输入就会作为一个 fontSize 为 undefined 的 Settings 一路传下去。对于大型结构,校验库能完成同样的工作,还能帮你推导出类型。
noImplicitAny
any 不只来自有人手写。一个既没有注解、也没有上下文可供推断的参数同样会是 any。noImplicitAny 选项(strict 会开启它)会把这种情况报告为错误:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
解决办法是加上注解:function double(x: number)。显式写 x: any 也能通过编译,而这正是目的所在:any 在代码中保持可见,可以被搜索和审查。
any 仍然会悄悄混进来的地方
即使在 strict 下,下面这些情况也会产生 any,而你的代码里根本没出现这个词:
| 来源 | 你得到的类型 | 怎么办 |
|---|---|---|
JSON.parse(text) | any | 标注为 unknown,然后校验 |
使用浏览器(DOM)类型时的 response.json() | Promise<any> | 与 JSON.parse 相同(Node 自带的 fetch 类型已经返回 Promise<unknown>) |
| 没有类型定义的包 | 它的导入都是 any(除非你添加声明,否则会报错) | 安装 @types/... 或编写 .d.ts |
value as any | any | 使用类型守卫或精确的断言 |
关闭 useUnknownInCatchVariables 时的 catch (e) | any | strict 会让它变成 unknown,保持这样即可 |
在 strict 下,catch 变量是 unknown,因为被抛出的可以是任何东西,不只是 Error 对象。读取 e.message 之前,先用 e instanceof Error 收窄它。
什么时候可以用 any
any 并没有被禁止,但每一处 any 都是编译器不再保护的地方。合理的用法有:
- 迁移 JavaScript 代码库时,用
any标记还没加类型的部分。 - 类型系统难以表达的代码,保持很小的范围,并藏在一个有类型的函数签名后面。
- 故意传入错误数据的测试代码。
其他所有情况,unknown 都能覆盖同样的“我不知道这个类型”的场景,同时保留检查。很多团队用 @typescript-eslint/no-explicit-any 这条 lint 规则来强制执行这一点。一个相关的类型 Record<string, unknown> 是表示“值未知的某个对象”的常用写法。
常见问题
TypeScript 中 any 和 unknown 有什么区别?
两者都能接受任何值。对 any 类型的值,你可以做任何事(读取属性、调用它、把它赋给 number),编译器什么都不检查。对 unknown 类型的值,在用 typeof x === "string" 这样的检查收窄之前,你几乎什么都做不了。unknown 保留了类型检查,所以它是更安全的选择。
什么时候应该用 unknown 而不是 any?
只要值的类型在编译时无法确定就用它:解析出来的 JSON、网络响应的数据、捕获到的错误、校验函数的输入。把它标注为 unknown,再进行收窄。只有在短期的迁移工作中,或者类型系统描述不了的代码里,才使用 any。
"Object is of type 'unknown'" 是什么意思?
错误 TS18046('x' is of type 'unknown')和 TS2571(Object is of type 'unknown',在值不是一个简单名称时出现,比如 load().id)表示你把 unknown 值当作某个具体类型来用了,例如读取了它的属性或调用了它的方法。先检查类型(typeof、instanceof、Array.isArray、in 或类型守卫函数),再在收窄后的分支里使用这个值。
为什么 JSON.parse 返回 any?
标准库中它的声明是 parse(text: string, ...): any,因为编译器无法知道一个字符串里装的是什么。这个结果会悄悄关闭它所涉及的一切的类型检查。正确的做法是给它加注解:const data: unknown = JSON.parse(text),使用前先校验。
noImplicitAny 是什么?
它是一个编译选项,属于 strict 的一部分。当一个声明既没有类型注解、也没有可供推断的信息,因而会悄悄变成 any 类型时,它会报错:参数报 TS7006,类型无法确定的变量报 TS7005 或 TS7034。它能防止 any 在没人写的情况下出现。显式写出的 any 仍然是允许的。