Menu

TypeScript 函数类型:箭头语法与回调类型

如何在 TypeScript 中写出函数的类型:箭头语法 (a: number) => string、函数的类型别名、为箭头函数和回调标注类型、调用签名与构造签名,以及为什么 Function 类型太宽松。

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

函数类型描述函数的参数和返回值,用箭头来写:(a: number, b: number) => number。用 type 给它起个名字,就能把它用于变量、参数和对象属性。

赋值给 add 的函数不需要任何标注:它的参数类型来自 Operation。这叫作上下文类型推断,也是给函数类型命名的主要原因。

函数类型的语法

形式是 (parameters) => ReturnType。每个参数都需要一个名称和一个类型。名称是为了可读性和编辑器提示;只要参数类型兼容,任何函数都能匹配,不管它的参数叫什么。

type Predicate = (value: number) => boolean;
type Formatter = (value: number, digits?: number) => string; // optional parameter
type Logger = (...parts: string[]) => void;                    // rest parameter
type Factory = () => { id: number };                           // no parameters

名称不能省略。(string) => void 声明的是一个名为 string、没有类型的参数,严格模式会拒绝它,报错 TS7051: Parameter has a name but no type. Did you mean 'arg0: string'?。应该写成 (value: string) => void。

给箭头函数标注类型

给箭头函数标注类型有两种方式。直接标注函数本身,或者给变量标注一个函数类型,让参数类型自动推断。

对于独立的函数,通常选择第 1 种写法。当多个函数共用同一个签名时(处理函数、比较函数、转换函数),第 3 种写法就显出优势了,因为签名只写一次,修改它时每一处都会被检查。

回调类型

类型为函数类型的参数就是回调。调用方传入的函数会按这个类型检查,函数的参数类型也由它推断而来。

有两条规则让回调用起来很顺手:

  • 参数少一些没关系。 只接受一个参数的函数,可以传到提供两个参数的地方。JavaScript 会忽略多余的实参,所以 TypeScript 允许这样做。参数比类型提供的多则会报错(Target signature provides too few arguments.)。
  • void 返回类型可以接受任何返回值。 类型为 (...) => void 的回调可以返回值;调用方保证不会使用它。所以即使 push 返回一个数字,list.forEach((x) => other.push(x)) 也能通过编译。

参数类型仍然必须兼容。在需要 (a: number, b: number) => number 的地方传入 (x: string) => ...,会报错 Types of parameters 'x' and 'a' are incompatible.

调用签名

(n: number) => string 是带调用签名的对象类型 { (n: number): string } 的简写。当函数还带有属性时,就需要完整的写法。

TypeScript 允许你在同一作用域中给用 const 声明的函数添加属性,并会在函数的类型中记录它们。接口也可以包含调用签名:interface Counter { (): number; count: number }。

构造签名描述的是用 new 调用的东西。它在前面加上 new:new (name: string) => User,对象形式则是 { new (name: string): User }。给接收类的参数标注类型时就用它。

方法语法与属性语法

在对象类型中,函数成员可以写成方法 handle(value: string): void,也可以写成存放函数的属性 handle: (value: string) => void。它们看起来可以互换,但在 strict 下的检查方式不同:

编译器报告:

index.ts(8,27): error TS2322: Type '(value: string) => void' is not assignable to type '(value: string | number) => void'.
  Types of parameters 'value' and 'value' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

这个报错是对的:onlyStrings 处理不了数字。方法写法会放过同样的错误(由于历史原因,方法参数是“双变”检查的),而 a.handle(42) 会在运行时崩溃,报错 TypeError: value.toUpperCase is not a function。删掉 b 那一行就能看到这个崩溃。编写自己的对象类型时,属性写法能捕获更多错误。

避免使用 Function 类型

Function 是所有函数值都满足的内置类型。它几乎从来都不是你想要的:TypeScript 不知道它的参数和返回类型,所以每次调用都会被接受,并返回 any。

把 Function 换成真实的签名。如果你确实要接受任何函数(通用的 debounce、日志包装函数),(...args: never[]) => unknown 能接受所有函数,同时让结果保持为 unknown 而不是 any。对于类型取决于输入的函数,见泛型:泛型函数类型的写法是 <T>(value: T) => T。

常见问题

TypeScript 中如何定义函数类型?

使用箭头语法:(a: number, b: number) => number。用类型别名给它起个名字,type Compare = (a: number, b: number) => number;,然后把这个名字用于变量、参数和属性。参数名是语法的一部分,但只起说明作用:参数名不同的函数照样匹配。

TypeScript 中如何给回调参数标注类型?

把函数类型写成参数的类型:function onEach(items: string[], cb: (item: string, index: number) => void) { ... }。调用方可以传入参数更少的函数,回调自身的参数类型也会从这个类型推断出来,所以 onEach(list, (item) => ...) 不需要任何标注。

为什么在 TypeScript 中不应该使用 Function 类型?

Function 接受任何函数,而且调用它时不做检查:任何参数都被允许,结果是 any。应该写出真实的签名,例如 () => void 或 (value: string) => number;如果你确实要接受所有函数,就写 (...args: never[]) => unknown。

函数类型和调用签名有什么区别?

它们描述的是同一个东西。(n: number) => string 是对象类型 { (n: number): string } 的简写。当函数还带有属性时,就需要调用签名的写法,例如 { (n: number): string; label: string }。

TypeScript 中如何给箭头函数标注类型?

要么直接在函数上标注参数和返回类型,const half = (n: number): number => n / 2;,要么给变量一个函数类型,让参数类型自动推断:const half: (n: number) => number = (n) => n / 2;。

Coddy 编程语言插图

和 Coddy 一起学编程

开始