本篇文章将介绍如何为函数添加类型,包括参数类型、返回值类型、this类型以及函数重载等。
1. 常规参数类型
在函数形式参数列表中,为参数添加类型注解就能够定义参数的类型。例如,下例中将add函数声明中的参数x和参数y的类型都定义为number类型:
function add(x: number, y: number) {
return x + y;
}
如果在函数形式参数列表中没有明确指定参数类型,并且编译器也无法推断参数类型,那么参数类型将默认为any类型。示例如下:
// 参数x和y隐式地获得了'any'类型
function add(x, y) {
return x + y;
}
如果启用了“--noImplicitAny”编译选项,那么此例中的代码将会产生编译错误。我们必须指明参数的类型,如果期望的类型就是any类型,则需要使用类型注解来明确地标注。
2. 可选参数类型
在JavaScript中,函数的每一个参数都是可选参数,而在TypeScript中,默认情况下函数的每一个参数都是必选参数。在调用函数时,编译器会检查传入实际参数的个数与函数定义中形式参数的个数是否相等。如果两者不相等,则会产生编译错误。如果一个参数是可选参数,那么就需要在函数类型定义中明确指定。
在函数形式参数名后面添加一个问号“?”就可以将该参数声明为可选参数。例如,下例中我们将add函数的参数y定义为可选参数:
function add(x: number, y?: number) {
return x + (y ?? 0);
}
函数的可选参数必须位于函数参数列表的末尾位置。在可选参数之后不允许再出现必选参数,否则将产生编译错误。例如,下例中add函数的第一个参数x是可选参数,在它之后的参数y是必选参数,因此将产生编译错误。
如果函数的某个参数是可选参数,那么在调用该函数时既可以传入对应的实际参数,也可以完全不传入任何实际参数。例如,下例中参数x是必选参数,y是可选参数。在调用add函数时,既可以传入一个实际参数,也可以传入两个实际参数。但是,若没有传入参数或者传入了多于两个的参数,则将产生编译错误。
在“--strictNullChecks”模式下,TypeScript会自动为可选参数添加undefined类型。因此,上例中add函数的定义等同于如下定义:
/**
* --strictNullChecks=true
*/
function add(x: number, y?: number | undefined) {
return x + (y ?? 0);
}
3. 默认参数类型
函数默认参数类型可以通过类型注解定义,也可以根据默认参数值自动地推断类型。例如,下例中函数默认参数x的类型通过类型注解明确定义,而默认参数y的类型则是根据默认值0推断出的类型,最后两个参数的类型均为number类型。示例如下:
function add(x: number = 0, y = 0) {
return x + y;
}
4.剩余参数类型
必选参数、可选参数和默认参数处理的都是单个参数,而剩余参数处理的则是多个参数。如果函数定义中声明了剩余参数,那么在调用函数时会将多余的实际参数收集到剩余参数列表中。因此,剩余参数的类型应该为数组类型或元组类型。
- 数组类型的剩余参数
最常见的做法是将剩余参数的类型声明为数组类型。例如,下例中的f函数定义了“number[]”类型的剩余参数,在调用定义了剩余参数的函数时,剩余参数可以接受零个或多个实际参数。
function f(...args: number[]) {}
f();
f(0);
f(0, 1);
- 元组类型的剩余参数
剩余参数的类型也可以定义为元组类型。例如,下例中剩余参数args的类型为包含两个元素的元组类型:
function f(...args: [boolean, number]) {}
5.解构参数类型
可以使用类型注解为解构参数添加类型信息。示例如下:
function f0([x, y]: [number, number]) {}
f0([0, 1]);
function f1({ x, y }: { x: number; y: number }) {}
f1({ x: 0, y: 1 });
6.返回值类型
在函数形式参数列表之后,可以使用类型注解为函数添加返回值类型。例如,下例中定义了add函数的返回值类型为number类型:
//函数返回值类型
function add(x: number, y: number): number {
return x + y;
}
7.函数类型字面量
函数类型字面量是定义函数类型的方法之一,它能够指定函数的参数类型、返回值类型以及泛型类型参数。函数类型字面量的语法与箭头函数的语法相似,具体语法如下所示:
(ParameterList) => Type
在该语法中,ParameterList表示可选的函数形式参数列表;Type表示函数返回值类型;形式参数列表与返回值类型之间使用胖箭头“=>”连接。
下例中,变量f的类型为函数类型,这代表变量f的值是一个函数。该函数类型通过函数类型字面量进行定义,表示一个不接受任何参数且返回值类型为void的函数。示例如下:
// 函数类型字面量
let f: () => void;
f = function () { /* no-op */ };
8. 重载函数
重载函数是指一个函数同时拥有多个同类的函数签名。例如,一个函数拥有两个及以上的调用签名,或者一个构造函数拥有两个及以上的构造签名。当使用不同数量和类型的参数调用重载函数时,可以执行不同的函数实现代码。
TypeScript中的重载函数与其他编程语言中的重载函数略有不同。首先,让我们看一个重载函数的例子。下例中定义了一个重载函数add。它接受两个参数,若两个参数的类型为number,则返回它们的和;若两个参数的类型为数组,则返回合并后的数组。在调用add函数时,允许使用这两个调用签名之一并且能够得到正确的返回值类型。示例如下:
function add(x: number, y: number): number;
function add(x: any[], y: any[]): any[];
function add(x: number | any[], y: number | any[]): any {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
}
if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
}
const a: number = add(1, 2);
const b: number[] = add([1], [2]);
在使用函数声明定义函数时能够定义重载函数。重载函数的定义由以下两部分组成:
- 一条或多条函数重载语句。
- 一条函数实现语句。
函数重载
不带有函数体的函数声明语句叫作函数重载。例如,下例中的add函数声明没有函数体,因此它属于函数重载:
function add(x: number, y: number): number;
函数重载的语法中不包含函数体,它只提供了函数的类型信息。函数重载只存在于代码编译阶段,在编译生成JavaScript代码时会被完全删除,因此在最终生成的JavaScript代码中不包含函数重载的代码。
函数重载允许存在一个或多个,但只有多于一个的函数重载才有意义,因为若只有一个函数重载,则可以直接定义函数实现。在函数重载中,不允许使用默认参数。函数重载应该位于函数实现(将在下一节中介绍)之前,每一个函数重载中的函数名和函数实现中的函数名必须一致。
在各个函数重载语句之间以及函数重载语句与函数实现语句之间不允许出现任何其他语句,否则将产生编译错误。示例如下:
function add(x: number, y: number): number;
const a = 0; // <-- 编译错误
function add(x: any[], y: any[]): any[];
const b = 0; // <-- 编译错误
function add(x: number | any[], y: number | any[]): any {
// 实现代码
}
函数实现
函数实现包含了实际的函数体代码,该代码不仅在编译时存在,在编译生成的JavaScript代码中同样存在。每一个重载函数只允许有一个函数实现,并且它必须位于所有函数重载语句之后,否则将产生编译错误。
TypeScript中的重载函数最令人迷惑的地方在于,函数实现中的函数签名不属于重载函数的调用签名之一,只有函数重载中的函数签名能够作为重载函数的调用签名。例如,下例中的add函数只有两个调用签名,分别为第1行与第2行定义的两个重载签名,而第3行函数实现中的函数签名不是add函数的调用签名,如下所示:
function add(x: number, y: number): number;
function add(x: any[], y: any[]): any[];
function add(x: number | any[], y: number | any[]): any {
// 实现代码
}
函数实现需要兼容每个函数重载中的函数签名,函数实现的函数签名类型必须能够赋值给函数重载的函数签名类型。示例如下:
// 编译错误:重载签名与实现签名的返回值类型不匹配
function foo(x: number): boolean;
// 编译错误:重载签名与实现签名的参数类型不匹配
function foo(x: string): void;
function foo(x: number): void {
// 函数体代码
}
在其他一些编程语言中允许存在多个函数实现,并且在调用重载函数时编程语言负责选择合适的函数实现执行。在TypeScript中,重载函数只存在一个函数实现,开发者需要在这个唯一的函数实现中实现所有函数重载的功能。这就需要开发者自行去检测参数的类型及数量,并根据判断结果去执行不同的操作。
函数重载解析顺序
当程序中调用了一个重载函数时,编译器将首先构建出一个候选函数重载列表。一个函数重载需要满足如下条件才能成为本次函数调用的候选函数重载:
- 函数实际参数的数量不少于函数重载中定义的必选参数的数量。
- 函数实际参数的数量不多于函数重载中定义的参数的数量。
- 每个实际参数的类型能够赋值给函数重载定义中对应形式参数的类型。
候选函数重载列表中的成员将以函数重载的声明顺序作为初始顺序,然后进行简单的排序,将参数类型中包含字面量类型的函数重载排名提前。
function f(x: string): void; // <- 函数重载1
function f(y: 'specialized'): void; // <- 函数重载2
function f(x: string) {
// 省略函数体代码
}
f('specialized');
上例中使用字符串参数'specialized'调用重载函数f时,函数重载1和函数重载2都满足候选函数重载的条件,因此两者都在候选函数重载列表中。但是因为函数重载2的函数签名中包含字面量类型,所以比函数重载1的优先级更高。
重载函数的类型
重载函数的类型可以通过包含多个调用签名的对象类型来表示。例如,有以下重载函数定义:
function f(x: string): 0 | 1;
function f(x: any): number;
function f(x: any): any {
// ...
}
我们可以使用如下对象类型字面量来表示重载函数f的类型。在该对象类型字面量中,定义了两个调用签名类型成员,分别对应于重载函数的两个函数重载。示例如下:
{
(x: string): 0 | 1;
(x: any): number;
}
在定义重载函数的类型时,有以下两点需要注意:
- 函数实现的函数签名不属于重载函数的调用签名之一。
- 调用签名的书写顺序是有意义的,它决定了函数重载的解析顺序,一定要确保更精确的调用签名位于更靠前的位置。
网友评论