Menu

TypeScript 类型推断:何时需要写类型注解

TypeScript 能从你写的值推断出大部分类型。本文讲解它如何推断变量、let 与 const、对象、数组和返回值的类型,回调函数如何从上下文获得类型,以及哪些地方仍然需要类型注解。

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

类型推断就是 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 和大多数编辑器会在提示框中显示推断出的类型)。在代码里,把这个值赋给一个类型明显不对的变量,编译器就会在错误信息中打印出推断出的类型。

Coddy 编程语言插图

和 Coddy 一起学编程

开始