TypeScript 有两条路径知道类型:类型注解(你写出来)和类型推断(编译器算出来)。判断该不该写注解的标准只有一条:类型信息是否从值的右侧流到了左侧。
1// 什么时候必须写注解
2
3// 1. 函数参数:必须注解(无初始化表达式可推断)
4function add(a: number, b: number): number { return a + b; }
5
6// 2. 声明后延迟赋值:必须注解
7let pending: string;
8pending = "later";
9
10// 3. 空数组/空对象:建议注解
11// 空数组推断为 evolving any[](TS7034),push 后收窄,但没 push 就是 any[]
12const arrNoAnn = [];
13arrNoAnn.push(1);
14const narrowed: number[] = arrNoAnn; // 收窄为 number[]
15
16// 不注解且没用到时,类型是 any[],会带出 TS7034
17const emptyArr: number[] = []; // 显式注解更清楚
18const emptyObj: Record<string, number> = {};
19
20// 4. 需要收窄联合类型时:必须注解
21let id: string | number = "a";
22id = 1;
23
24// 5. 函数返回类型:可选,但公开 API 建议写
25export function parse(text: string): number { return Number(text); }
26
27// 6. 推断失败时:必须注解
28function make(): string { return "x"; }
29const made: string = make();
30
31// ---- 以下情况注解是噪音 ----
32
33// 有初始化表达式且类型显而易见
34const n = 1; // 不需要 : number
35const s = "a"; // 不需要 : string
36const arr = [1, 2, 3]; // 不需要 : number[]
37const obj = { a: 1 }; // 不需要 : { a: number }
38
39// 回调参数能从上下文推断
40[1, 2, 3].map((x) => x * 2); // x 自动推断为 number
41const nums: number[] = [1];
42nums.filter((x) => x > 0); // x 自动推断为 number
43
44// 类的字段有初始化
45class C {
46 count = 0; // 不需要 : number
47}
48
49console.log(add(1, 2), pending, emptyArr, emptyObj, id, parse("3"), made, n, s, arr, obj, new C().count);
50console.log(narrowed, arrNoAnn);
13 later [] {} 1 3 x 1 a [ 1, 2, 3 ] { a: 1 } 0
2[ 1 ] [ 1 ]
必须写注解的五种情况
| 情况 | 例子 | 为什么 |
|---|---|---|
| 函数参数 | function add(a: number, b: number) | 没有初始化表达式,无从推断 |
| 声明后延迟赋值 | let pending: string; | 同上 |
| 空数组/空对象 | const arr: number[] = [] | 推断不出元素类型 |
| 需要收窄联合 | let id: string | number = "a" | 默认推断为 string,太窄 |
| 公开 API 返回值 | export function parse(): number | 推断得出,但写出来能防止实现改动意外改变契约 |
函数参数是唯一强制要求注解的地方(noImplicitAny 下)。其余都是"建议"或"按需"。
不写注解的四种情况
1const n = 1; // 不需要 : number
2const arr = [1, 2, 3]; // 不需要 : number[]
3const obj = { a: 1 }; // 不需要 : { a: number }
4
5[1, 2, 3].map((x) => x * 2); // x 自动推断为 number
回调参数能从上下文推断,这是最常被多写注解的地方。map、filter、forEach 的参数都有上下文类型,不需要手写。
判断标准
如果注解和右侧的值完全重复(const n: number = 1),那是噪音。如果注解表达了右侧看不出来的信息(let id: string | number = "a"),那是必要的。
空容器的推断陷阱
空数组的推断行为比较特殊:
1const arrNoAnn = [];
2arrNoAnn.push(1);
3const narrowed: number[] = arrNoAnn; // 收窄为 number[]
这是 evolving any[]:声明时是 any[],push 之后收窄。如果一直没 push 就用到它,会报:
1error TS7034: Variable 'emptyArr' implicitly has type 'any[]' in some locations where its type cannot be determined.
和 let b; 的 evolving any 是同一机制(见 类型基础)。空数组建议直接注解,意图更清楚。
空对象字面量 {} 的类型就是 {},它可以赋给 Record<string, number>,但不能赋给 { a: number }:
1error TS2741: Property 'a' is missing in type '{}' but required in type '{ a: number; }'.
返回值注解该不该写
推断能得出返回类型,所以技术上不写也行。但有两种情况建议写:
情况一:公开 API。写了返回类型后,实现改动如果意外改变了返回类型,会在函数内部报错,而不是在调用方报错——错误离源头更近。
1// 建议写:改动实现时立刻发现契约变化
2export function parse(text: string): number { return Number(text); }
3
4// 如果实现改成 return text,调用方会先炸
情况二:递归函数。递归的返回类型推断需要先知道返回类型,不注解可能报 TS7023: 'f' implicitly has return type 'any'。
1function fib(n: number): number { // 不注解这里会报错
2 return n < 2 ? n : fib(n - 1) + fib(n - 2);
3}
其余情况(内部小函数、回调)不写更简洁。