函数类型

2026-09-09 00:00    #typescript  

函数是 TypeScript 里类型最复杂的构造之一:参数的变型规则、this 的类型、重载签名与实现签名的分离,每一条都有反直觉的地方。

参数逆变与返回值协变

 1// 函数参数的逆变(contravariance)
 2// 参数类型更宽的函数,可以赋值给参数类型更窄的位置
 3
 4interface BaseEvent { type: string }
 5interface ClickEvent extends BaseEvent { x: number; y: number }
 6
 7type Handler = (e: ClickEvent) => void;
 8
 9// BaseEvent 比 ClickEvent 宽,所以可以当 ClickEvent 的处理器
10const wide: Handler = (e: BaseEvent) => { void e; };
11
12// 反过来不行:只接受 ClickEvent 的函数不能处理任意 BaseEvent
13// @ts-expect-error TS2322: 参数类型不兼容
14const narrow: (e: BaseEvent) => void = (e: ClickEvent) => { void e; };
15
16// 返回值是协变的:返回类型更窄的函数可以赋给更宽的位置
17type Factory = () => BaseEvent;
18const specific: Factory = () => ({ type: "click", x: 1, y: 2 } as ClickEvent);
19
20// 方法简写 vs 函数属性:bivariance 差异(strictFunctionTypes 只作用于属性写法)
21interface WithMethod {
22  handle(e: BaseEvent): void;      // 方法简写:双变(bivariant)
23}
24interface WithProperty {
25  handle: (e: BaseEvent) => void;  // 属性写法:逆变(contravariant)
26}
27
28// 方法简写更宽松:允许传"更窄参数"的函数(双变的宽松方向)
29const m: WithMethod = { handle: (e: ClickEvent) => { void e; } };
30
31// 属性写法在 strictFunctionTypes 下拒绝同样的写法
32// @ts-expect-error TS2322: 属性写法要求参数逆变
33const p: WithProperty = { handle: (e: ClickEvent) => { void e; } };
34
35// 两种写法都允许"更宽参数"(逆变方向,安全)
36const m2: WithMethod = { handle: (e: { type: string }) => { void e; } };
37const p2: WithProperty = { handle: (e: { type: string }) => { void e; } };
38
39// 回调场景:参数能宽不能窄
40function onClick(cb: (e: ClickEvent) => void): void {
41  cb({ type: "click", x: 1, y: 2 });
42}
43onClick((e) => { void e; });              // e 推断为 ClickEvent
44onClick((e: BaseEvent) => { void e; });   // 显式写宽也可以
45// onClick((e: ClickEvent & { z: 1 }) => {});   // 更窄不行
46
47console.log(typeof wide, typeof narrow, typeof specific, typeof m, typeof p, typeof m2, typeof p2, typeof onClick);
1function function function object object object object function

核心规则:参数类型更宽的函数,可以赋值给参数类型更窄的位置。

1interface BaseEvent { type: string }
2interface ClickEvent extends BaseEvent { x: number; y: number }
3
4type Handler = (e: ClickEvent) => void;
5
6const wide: Handler = (e: BaseEvent) => {};      // ✓ 参数更宽,安全
7const narrow: (e: BaseEvent) => void = (e: ClickEvent) => {};
8// error TS2322: Types of parameters 'e' and 'e' are incompatible.

为什么?因为 Handler 会被用 ClickEvent 调用。如果实现只接受 ClickEvent,那没问题;但如果实现接受 BaseEvent,也能处理 ClickEvent(因为它是子类型)。反过来,如果实现只接受比 ClickEvent 更窄的类型,就无法处理所有 ClickEvent。

实测错误消息:

1error TS2322: Type '(e: BaseEvent) => void' is not assignable to type '(e: ClickEvent) => void'.
2  Types of parameters 'e' and 'e' are incompatible.
3    Property 'type' is missing in type 'ClickEvent' but required in type 'BaseEvent'.

返回值方向相反——返回更窄类型是安全的:

1type Factory = () => BaseEvent;
2const specific: Factory = () => ({ type: "click", x: 1, y: 2 });   // ✓ 返回子类型

方法简写 vs 属性写法

这是 strictFunctionTypes 的一个例外,很容易踩:

 1interface WithMethod {
 2  handle(e: BaseEvent): void;      // 方法简写:双变(bivariant)
 3}
 4interface WithProperty {
 5  handle: (e: BaseEvent) => void;  // 属性写法:逆变
 6}
 7
 8// 方法简写允许传"更窄参数"的函数
 9const m: WithMethod = { handle: (e: ClickEvent) => {} };
10
11// 属性写法拒绝
12const p: WithProperty = { handle: (e: ClickEvent) => {} };
13// error TS2322: Type '(e: ClickEvent) => void' is not assignable to type '(e: BaseEvent) => void'.

结论:方法简写比属性写法宽松。这是历史原因(为了让 Array 等内置类型的方法可用),但意味着:

想要严格的逆变检查,用属性写法

如果类型安全对你很重要,把接口里的方法写成属性形式 handle: (e: T) => void,而不是方法简写 handle(e: T): void。strictFunctionTypes 只对属性写法生效。

this 参数

 1// this 参数:显式声明 this 的类型
 2
 3// 普通函数里的 this 默认是 any(noImplicitThis 下报错)
 4function bad() {
 5  // @ts-expect-error TS2683: 'this' 隐式具有 any 类型
 6  return this.value;
 7}
 8
 9// 用 this 参数显式声明(必须是第一个参数,只是类型声明不占实参位)
10function good(this: { value: number }): number {
11  return this.value;
12}
13
14// 调用时用 call/apply 绑定
15const result = good.call({ value: 1 });
16
17// 对象方法里的 this:TS 能推断出来
18const obj = {
19  value: 42,
20  get() {
21    return this.value;   // this 推断为 obj 的类型
22  },
23};
24
25// 类里的 this 是实例类型
26class Counter {
27  value = 0;
28  inc(): number {
29    return ++this.value;
30  }
31}
32
33// this 参数与泛型结合:链式调用
34interface Chainable {
35  add(this: Chainable, n: number): this;
36}
37
38// 事件处理器风格的 this
39interface Widget {
40  el: string;
41  onClick(this: Widget, e: { type: string }): void;
42}
43
44const widget: Widget = {
45  el: "div",
46  onClick(e) { console.log(this.el, e.type); },
47};
48widget.onClick({ type: "click" });
49
50// 显式排除 this 的使用:this: void
51function standalone(this: void, x: number): number {
52  return x * 2;
53}
54
55// 类型工具:ThisParameterType / OmitThisParameter
56type ThisType = ThisParameterType<typeof good>;      // { value: number }
57type NoThis = OmitThisParameter<typeof good>;        // () => number
58const noThis: NoThis = good.bind({ value: 1 });
59
60const thisTypeCheck: ThisType = { value: 1 };
61
62console.log(result, obj.get(), new Counter().inc(), standalone(2), noThis(), thisTypeCheck.value, bad.call({ value: 3 }));
1div click
21 42 1 4 1 1 3

this 默认是 any,noImplicitThis(strict 包含)下报错:

1function bad() {
2  return this.value;
3  // error TS2683: 'this' implicitly has type 'any' because it does not have a
4  // type annotation.
5}

显式声明用伪参数语法——它写在参数列表第一位,但不占实参位置:

1function good(this: { value: number }): number {
2  return this.value;
3}
4
5good.call({ value: 1 });    // 用 call/apply 绑定
6// good({ value: 1 });      // 错误:this 参数不占实参位

this: void 表示"这个函数不能用 this":

1function standalone(this: void, x: number): number { return x * 2; }

两个类型工具:

1type T = ThisParameterType<typeof good>;      // { value: number }
2type N = OmitThisParameter<typeof good>;      // () => number

函数重载

 1// 函数重载:多个调用签名 + 一个实现签名
 2
 3// 重载签名(对调用方可见)
 4function format(value: string): string;
 5function format(value: number): string;
 6function format(value: Date): string;
 7// 实现签名(对调用方不可见,必须兼容所有重载)
 8function format(value: string | number | Date): string {
 9  if (typeof value === "string") return value.toUpperCase();
10  if (typeof value === "number") return value.toFixed(2);
11  return value.toISOString();
12}
13
14// 重载顺序很重要:先匹配到的优先
15function pick(x: string): "string";
16function pick(x: number): "number";
17function pick(x: string | number): "string" | "number" {
18  return typeof x === "string" ? "string" : "number";
19}
20
21// 顺序反了会导致宽签名先匹配,窄签名永远用不上
22function badPick(x: string | number): "wide";
23function badPick(x: string): "narrow";
24function badPick(x: string | number): "wide" | "narrow" {
25  return typeof x === "string" ? "narrow" : "wide";
26}
27const badResult = badPick("a");   // 推断为 "wide",不是 "narrow"
28
29// 对象方法的重载
30interface Parser {
31  parse(text: string): number;
32  parse(text: number): string;
33}
34
35const parser: Parser = {
36  parse(text: string | number): any {
37    return typeof text === "string" ? Number(text) : String(text);
38  },
39};
40
41// 用箭头函数实现重载:需要用类型断言或接口
42type Overloaded = {
43  (x: string): string;
44  (x: number): number;
45};
46const impl: Overloaded = ((x: string | number) => x) as Overloaded;
47
48// 重载 vs 联合类型:什么时候用重载
49// 用重载:返回值类型依赖入参类型
50function firstOrNull(arr: []): null;
51function firstOrNull<T>(arr: T[]): T;
52function firstOrNull<T>(arr: T[]): T | null {
53  return arr.length ? arr[0]! : null;
54}
55const r1 = firstOrNull([]);          // null
56const r2 = firstOrNull([1, 2]);      // number
57
58// 用联合更简单:返回值不依赖入参
59function simple(x: string | number): string {
60  return String(x);
61}
62
63console.log(format("a"), format(1), format(new Date("2020-01-01")));
64console.log(pick("a"), pick(1), badResult);
65console.log(parser.parse("1"), parser.parse(1), impl("a"), impl(1));
66console.log(r1, r2, simple("x"));
1A 1.00 2020-01-01T00:00:00.000Z
2string number narrow
31 1 a 1

重载的结构是多个调用签名 + 一个实现签名:

1function format(value: string): string;      // 重载签名(调用方可见)
2function format(value: number): string;
3function format(value: Date): string;
4function format(value: string | number | Date): string {   // 实现签名(调用方不可见)
5  // ...
6}

实现签名不对外可见,且必须兼容所有重载签名。

重载顺序决定匹配结果

TypeScript 按声明顺序从上往下匹配,先匹配到的生效:

1function badPick(x: string | number): "wide";
2function badPick(x: string): "narrow";
3function badPick(x: string | number): "wide" | "narrow" {
4  return typeof x === "string" ? "narrow" : "wide";
5}
6
7const r = badPick("a");    // 类型是 "wide"!

注意这里有个危险的错位:类型说 "wide",但运行时返回 "narrow"。实测确认:

1const probe: "wide" = badPick("a");   // 编译通过
2// 但运行时 badPick("a") === "narrow"
把窄签名写在前面

宽签名放在前面会"吃掉"所有调用,窄签名永远匹配不到。这不会报错,但会让类型和运行时行为不一致。规则:更具体的签名写在更宽泛的前面。

什么时候该用重载

场景用什么
返回类型依赖入参类型重载
入参数量不同重载
返回值不依赖入参联合类型更简单
1// 用重载:空数组返回 null,非空返回元素类型
2function firstOrNull(arr: []): null;
3function firstOrNull<T>(arr: T[]): T;
4function firstOrNull<T>(arr: T[]): T | null {
5  return arr.length ? arr[0]! : null;
6}
7
8// 用联合:返回值不依赖入参
9function simple(x: string | number): string { return String(x); }

用箭头函数写重载

箭头函数不能有重载签名,要用类型别名 + 断言:

1type Overloaded = {
2  (x: string): string;
3  (x: number): number;
4};
5
6const impl = ((x: string | number) => x) as Overloaded;

as 在这里是必要的,因为实现签名的类型和重载集合不完全兼容。

参数的其他形式

1function greet(name: string, title?: string): string { }    // 可选参数(必须在末尾)
2function repeat(text: string, times = 2): string { }        // 默认参数(自动推断)
3function sum(...nums: number[]): number { }                 // 剩余参数
4function dist({ x, y }: { x: number; y: number }): number { }  // 解构参数(需整体注解)

解构参数必须整体注解,因为解构本身不提供类型信息。

相关