类型推断就是 TypeScript 替你算出你没写的类型。给变量赋初值,它就取这个值的类型;函数返回一个值,返回类型也随之确定。大多数 TypeScript 代码里的类型注解比人们想象的少得多。
注释里写的是在编辑器中悬停每个名字时显示的内容。最后一行是编译错误(TS2322),尽管 count 从未被注解;// @ts-expect-error 把它标记为预期中的错误,这样代码块的其余部分才能运行。
let 与 const:字面量拓宽
const 永远不会改变,所以 TypeScript 给它最窄的类型:字面量值本身。let 得到更宽的类型,因为它之后可能被重新赋值。
let a = "left"; // string
const b = "left"; // "left"
let c = 42; // number
const d = 42; // 42
const e = true; // true
当函数只接受特定的值时,这一点就很重要:
index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.
改用 const,或者给变量加上注解 let dir: "left" | "right" = "left",就能修复。字面量类型有单独的页面:字面量类型。
对象和数组会拓宽其成员
对象字面量的属性是可变的,所以即使对象本身是 const,属性也会像 let 一样被拓宽。数组元素也是如此。as const 会让一切变成只读,并保留字面量类型。
返回类型推断
函数的返回类型由它的 return 语句推断。有多个 return 时,结果是它们的联合类型。
对大多数函数来说,推断出的返回类型就够用了。如果希望函数签名成为一份约定,就在导出或公开的函数上显式写出返回类型:函数体内的改动一旦返回了不同的类型,报错就出现在函数本身,而不是某个遥远的调用处。
上下文类型:回调函数自动获得类型
参数不会根据函数的调用方式推断,但有两个例外。带默认值的参数取默认值的类型(function retry(times = 3) 中 times 是 number)。另外,在已经需要某个函数类型的位置写出的函数,会从上下文获得参数类型,所以回调函数的参数不需要注解。
forEach、filter、reduce、Promise 的回调、事件处理函数,以及任何传给已声明函数类型参数的函数,都适用同样的规则。
参数需要类型注解
如果没有默认值,也没有上面那样的上下文,TypeScript 不会通过观察函数的调用方式来猜测参数类型。开启 strict 时,没有注解的参数会报错 TS7006:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
写成 function double(x: number),代码块就会打印 42,返回类型 number 由 x * 2 推断得出。
最佳通用类型
对于包含不同种类值的数组字面量,TypeScript 推断出的是这些值类型的联合类型组成的数组。它不会去寻找你没提到的共同基类。
需要留意的是空数组。const list = [] 一开始是一个“演化中”的数组,类型随每次 push 增长;加上注解 const list: string[] = [] 能事先表明意图,也更清晰。
何时写类型注解
| 情况 | 需要注解吗? |
|---|---|
| 声明时已赋值的变量 | 不需要,值决定了类型 |
声明时没有赋值的变量(let result;) | 需要,let result: string; |
| 函数参数 | 需要(有上下文的回调或有默认值的除外) |
| 小型内部函数的返回类型 | 可选 |
| 导出或公开函数的返回类型 | 建议写,作为约定 |
| 之后再填充的空数组或空对象 | 需要,const ids: number[] = [] |
| 值需要保留字面量类型 | 用 as const |
| 值必须符合某个类型,但保留自己推断出的类型 | 用 satisfies |
查看推断结果最快的方法是在编辑器里悬停在名字上。没有编辑器时,把值赋给一个类型错误的变量,比如 const probe: boolean = mixed;,编译器的错误信息就会写出推断出的类型。
常见问题
TypeScript 中的类型推断是什么?
就是编译器替你算出你没写的类型。let count = 0 会让 count 的类型为 number,一个对两个数字返回 a + b 的函数,返回类型会被推断为 number。推断出的类型和手写的类型一样会被严格检查。
TypeScript 中需要给每个变量都写类型注解吗?
不需要。需要注解的是:函数参数(它们不会根据调用推断,只会从默认值或回调上下文推断)、希望 API 稳定时导出函数的返回类型,以及声明时没有赋值的变量。其余的交给 TypeScript 推断:const name: string = "Ada" 只是在重复编译器已经知道的信息。
为什么 const 推断出字面量类型,而 let 推断出 string?
const 永远不能重新赋值,所以 const dir = "left" 得到字面量类型 "left"。let 之后可能被赋成任何其他字符串,所以 let dir = "left" 被拓宽为 string。对象字面量的属性也会这样拓宽,因为属性可以重新赋值;用 as const 可以阻止拓宽。
为什么 TypeScript 提示参数隐式具有 'any' 类型?
这是 noImplicitAny(strict 的一部分)报出的错误 TS7006。TypeScript 不会根据函数的调用方式推断参数类型,所以没有注解的参数就是 any。加上注解 function double(x: number),或者把函数放在能从上下文得知其类型的位置,比如作为 map 的回调。
怎样查看 TypeScript 推断出的类型?
在编辑器中把鼠标悬停在名字上(VS Code 和大多数编辑器会在提示框中显示推断出的类型)。在代码里,把这个值赋给一个类型明显不对的变量,编译器就会在错误信息中打印出推断出的类型。