函数类型

在本节中,将介绍如何为函数添加类型,包括参数类型、返回值类型、this 类型以及函数重载等。

常规参数类型

在函数形式参数列表中,为参数添加类型注解就能够定义参数的类型。例如,下例中将 add 函数声明中的参数 x 和参数 y 的类型都定义为 number 类型:

function add(x: number, y: number) {
    return x + y;
}

针对函数表达式和匿名函数,我们也可以使用相同的方法来定义参数的类型。示例如下:

const f = function (x: number, y: number) {
    return x + y;
};

如果在函数形式参数列表中没有明确指定参数类型,并且编译器也无法推断参数类型,那么参数类型将默认为 any 类型。示例如下:

function add(x, y) {
//           ~~~~
//           参数x和y隐式地获得了'any'类型

    return x + y;
}

注意,如果启用了 --noImplicitAny 编译选项,那么此例中的代码将会产生编译错误。我们必须指明参数的类型,如果期望的类型就是 any 类型,则需要使用类型注解来明确地标注。示例如下:

function add(x: any, y: any) {
    return x + y;
}

可选参数类型

JavaScript 中,函数的每一个参数都是可选参数,而在 TypeScript 中,默认情况下函数的每一个参数都是必选参数。在调用函数时,编译器会检查传入实际参数的个数与函数定义中形式参数的个数是否相等。如果两者不相等,则会产生编译错误。如果一个参数是可选参数,那么就需要在函数类型定义中明确指定。

在函数形式参数名后面添加一个问号 ? 就可以将该参数声明为可选参数。例如,下例中我们将 add 函数的参数 y 定义为可选参数:

function add(x: number, y?: number) {
    return x + (y ?? 0);
}

我们也可以同时定义多个可选参数。示例如下:

function add(x: number, y?: number, z?: number) {
    return x + (y ?? 0) + (z ?? 0);
}

函数的可选参数必须位于函数参数列表的末尾位置。在可选参数之后不允许再出现必选参数,否则将产生编译错误。例如,下例中 add 函数的第一个参数 x 是可选参数,在它之后的参数 y 是必选参数,因此将产生编译错误。示例如下:

function add(x?: number, y: number) {
    //                   ~
    //                   编译错误!必选参数不能出现在可选参数之后
}

如果函数的某个参数是可选参数,那么在调用该函数时既可以传入对应的实际参数,也可以完全不传入任何实际参数。例如,下例中参数x是必选参数,y 是可选参数。在调用 add 函数时,既可以传入一个实际参数,也可以传入两个实际参数。但是,若没有传入参数或者传入了多于两个的参数,则将产生编译错误。示例如下:

function add(x: number, y?: number) {
    return x + (y ?? 0);
}

add();        // 编译错误
add(1);       // 正确
add(1, 2);    // 正确
add(1, 2, 3); // 编译错误

--strictNullChecks 模式下,TypeScript 会自动为可选参数添加 undefined 类型。因此,上例中 add 函数的定义等同于如下定义:

/**
 * --strictNullChecks=true
 */
function add(x: number, y?: number | undefined) {
    return x + (y ?? 0);
}

TypeScript 允许给可选参数传入一个 undefined 值。示例如下:

/**
 * --strictNullChecks=true
 */
function add(x: number, y?: number) {
    return x + (y ?? 0);
}

add(1);            // 1
add(1, 2);         // 3
add(1, undefined); // 1

需要注意的是,为参数添加 undefined 类型不等同于该参数是可选参数。若省略了 ? 符号,则参数将成为必选参数,在调用时必须传入一个实际参数值。

默认参数类型

函数默认参数类型可以通过类型注解定义,也可以根据默认参数值自动地推断类型。例如,下例中函数默认参数 x 的类型通过类型注解明确定义,而默认参数 y 的类型则是根据默认值 0 推断出的类型,最后两个参数的类型均为 number 类型。示例如下:

function add(x: number = 0, y = 0) {
    return x + y;
}

如果函数定义了默认参数,并且默认参数处于函数参数列表末尾的位置,那么该参数将被视为可选参数,在调用该函数时可以不传入对应的实际参数值。例如,下例中参数 y 是默认参数,且处于参数列表的末尾,因此参数 y 成了可选参数。在调用 add 函数时,允许不传入参数 y 的实际参数值。示例如下:

function add(x: number, y: number = 0) {
    return x + y;
}

add(1);    // 1
add(1, 2); // 3

在语法上,同一个函数参数不允许同时声明为可选参数和默认参数,否则将产生编译错误。示例如下:

function f(x?: number = 0) {
    //     ~
    //     编译错误!参数不能同时使用?符号和初始化值
}

如果默认参数之后存在必选参数,那么该默认参数不是可选的参数,在调用函数时必须传入对应的实际参数值。示例如下:

function add(x: number = 0, y: number) {
    return x + y;
}

add(1);            // 编译错误
add(1, 2);         // 正确
add(undefined, 2); // 正确

剩余参数类型

必选参数、可选参数和默认参数处理的都是单个参数,而剩余参数处理的则是多个参数。如果函数定义中声明了剩余参数,那么在调用函数时会将多余的实际参数收集到剩余参数列表中。因此,剩余参数的类型应该为数组类型或元组类型。虽然剩余参数也可以定义为顶端类型或尾端类型,但是实际意义不大,因此不展开介绍。

数组类型的剩余参数

最常见的做法是将剩余参数的类型声明为数组类型。例如,下例中的 f 函数定义了 number[] 类型的剩余参数:

function f(...args: number[]) {}

在调用定义了剩余参数的函数时,剩余参数可以接受零个或多个实际参数。示例如下:

function f(...args: number[]) {}

f();
f(0);
f(0, 1);

元组类型的剩余参数

剩余参数的类型也可以定义为元组类型。例如,下例中剩余参数 args 的类型为包含两个元素的元组类型:

function f(...args: [boolean, number]) {}

如果剩余参数的类型为元组类型,那么编译器会将剩余参数展开为独立的形式参数声明,主要包括以下几种情况:

  • 常规元组类型,示例如下:

    function f0(...args: [boolean, number]) {}
    
    // 等同于:
    
    function f1(args_0: boolean, args_1: number) {}
  • 带有可选元素的元组类型,示例如下:

    function f0(...args: [boolean, string?]) {}
    
    // 等同于:
    
    function f1(args_0: boolean, args_1?: string) {}
  • 带有剩余元素的元组类型,示例如下:

    function f0(...args: [boolean, ...string[]]) {}
    
    // 等同于:
    
    function f1(args_0: boolean, ...args_1: string[]) {}

在了解了元组类型剩余参数的展开行为后,我们也就清楚了该如何传入对应的实际参数,如下所示:

function f0(...args: [boolean, number, string]) {}
f0(true, 0, '');

function f1(...args: [boolean, number, string?]) {}
f1(true, 0, '');
f1(true, 0);

function f2(...args: [boolean, number, ...string[]]) {}
f2(true, 0);
f2(true, 0, '');
f2(true, 0, '', 'hello');

function f3(...args: [boolean, number?, ...string[]]) {}
f3(true);
f3(true, 0);
f3(true, 0, '');
f3(true, 0, '', 'hello');

解构参数类型

在4.3节中,我们介绍了如何对数组和对象进行解构。解构还可以应用在函数参数列表中。示例如下:

function f0([x, y]) {}
f0([0, 1]);

function f1({ x, y }) {}
f1({ x: 0, y: 1 });

我们可以使用类型注解为解构参数添加类型信息。示例如下:

function f0([x, y]: [number, number]) {}
f0([0, 1]);

function f1({ x, y }: { x: number; y: number }) {}
f1({ x: 0, y: 1 });

返回值类型

在函数形式参数列表之后,可以使用类型注解为函数添加返回值类型。例如,下例中定义了 add 函数的返回值类型为 number 类型:

function add(x: number, y: number): number {
//                                  ~~~~~~
//                                  函数返回值类型
    return x + y;
}

在绝大多数情况下,TypeScript 能够根据函数体内的 return 语句等自动推断出返回值类型,因此我们也可以省略返回值类型。示例如下:

function add(x: number, y: number) {
    return x + y;
}

此例中,我们没有为 add 函数添加返回值类型,但是 TypeScript 能够根据表达式 x + y 的类型推断出 add 函数的返回值类型为 number 类型。

TypeScript 的原始类型里有一个特殊的空类型 void,该类型唯一有意义的使用场景就是作为函数的返回值类型。如果一个函数的返回值类型为 void,那么该函数只能返回 undefined 值。这意味着函数明确地返回了一个 undefined 值,或者函数没有调用 return 语句,在这种情况下函数默认返回 undefined 值。示例如下:

// f0和f1是正确的使用场景

function f0(): void {
    return undefined;
}
function f1(): void {}


// f2, f3和f4是错误的使用场景

function f2(): void {
    return false;
}

function f3(): void {
    return 0;
}

function f4(): void {
    return '';
}

如果没有启用 --strictNullChecks 编译选项,那么 void 返回值类型也允许返回 null 值。示例如下:

/**
 * --strictNullChecks=false
 */
function f0(): void {
    return null;
}

函数类型字面量

在前面几节中,介绍了如何为现有函数添加参数和返回值类型。在本节中,我们将介绍如何使用函数类型字面量来描述某个函数的类型。

函数类型字面量是定义函数类型的方法之一,它能够指定函数的参数类型、返回值类型以及将在 6.1 节中介绍的泛型类型参数。函数类型字面量的语法与箭头函数的语法相似,具体语法如下所示:

(ParameterList) => Type

在该语法中,ParameterList 表示可选的函数形式参数列表;Type 表示函数返回值类型;形式参数列表与返回值类型之间使用胖箭头 => 连接。

下例中,变量 f 的类型为函数类型,这代表变量 f 的值是一个函数。该函数类型通过函数类型字面量进行定义,表示一个不接受任何参数且返回值类型为 void 的函数。示例如下:

let f: () => void;
//     ~~~~~~~~~~
//     函数类型字面量

f = function () { /* no-op */ };

在函数类型字面量中定义函数参数的类型时,必须包含形式参数名,不允许只声明参数的类型。下例中,add 函数是正确的定义方式,而 f 函数则是错误的定义方式。编译器会将 f 函数参数列表中的 number 当作参数名,而不是参数类型。示例如下:

let add: (x: number, y: number) => number;

let f: (number) => number;
//      ~~~~~~
//      编译错误

函数类型字面量中的形式参数名与实际函数值中的形式参数名不必相同。例如,下例中函数类型字面量中声明的形式参数名为 x,而实际函数值的形式参数名为 y

let f: (x: number) => number;

f = function (y: number): number {
    return y;
};

函数类型字面量中的返回值类型必须明确指定,不允许省略。如果函数没有返回值,则需要指定 void 类型作为返回值类型。示例如下:

let foo: () => void;

let bar: () => ;
//             ~~
//             编译错误:未指定返回值类型

调用签名

函数在本质上是一个对象,但特殊的地方在于函数是可调用的对象。因此,可以使用对象类型来表示函数类型。若在对象类型中定义了调用签名类型成员,那么我们称该对象类型为函数类型。调用签名的语法如下所示:

{
    (ParameterList): Type
}

在该语法中,ParameterList 表示函数形式参数列表类型,Type 表示函数返回值类型,两者都是可选的。

下例中,我们使用对象类型字面量和调用签名定义了一个函数类型,该函数类型接受两个 number 类型的参数,并返回 number 类型的值:

let add: { (x: number, y: number): number };

add = function (x: number, y: number): number {
    return x + y;
};

实际上,上一节介绍的函数类型字面量完全等同于仅包含一个类型成员并且是调用签名类型成员的对象类型字面量。换句话说,函数类型字面量是仅包含单个调用签名的对象类型字面量的简写形式,如下所示:

{ ( ParameterList ): Type }

// 简写为:

( ParameterList ) => Type

例如,Math.abs() 是一个内置函数,它接受一个数字参数并返回该参数的绝对值。下面,我们分别使用函数类型字面量和带有调用签名的对象类型字面量来定义 Math.abs() 函数的类型:

const abs0: (x: number) => number = Math.abs;

const abs1: { (x: number): number } = Math.abs;

abs0(-1) === abs1(-1);  // true

函数类型字面量的优点是简洁,而对象类型字面量的优点是具有更强的类型表达能力。我们知道函数是一种对象,因此函数可以拥有自己的属性。下例中,函数 f 除了可以被调用以外,还提供了一个 version 属性:

function f(x: number) {
    console.log(x);
}

f.version = '1.0';

f(1); // 1
f.version;   // '1.0'

若使用函数类型字面量,则无法描述 string 类型的 version 属性,因此也就无法准确地描述函数 f 的类型。示例如下:

function f(x: number) {
    console.log(x);
}
f.version = '1.0';

let foo: (x: number) => void = f;

const version = foo.version;
//                  ~~~~~~~
//                  编译错误:'(x: number) => void' 类型
//                  上不存在 'version' 属性

在这种情况下,我们可以使用带有调用签名的对象类型字面量来准确地描述函数 f 的类型。示例如下:

function f(x: number) {
    console.log(x);
}
f.version = '1.0';

let foo: { (x: number): void; version: string } = f;

const version = foo.version;  // string类型

构造函数类型字面量

在面向对象编程中,构造函数是一类特殊的函数,它用来创建和初始化对象。JavaScript 中的函数可以作为构造函数使用,在调用构造函数时需要使用 new 运算符。例如,我们可以使用内置的 Date 构造函数来创建一个日期对象,示例如下:

const date = new Date();

构造函数类型字面量是定义构造函数类型的方法之一,它能够指定构造函数的参数类型、返回值类型以及将在 6.1 节中介绍的泛型类型参数。构造函数类型字面量的具体语法如下所示:

new ( ParameterList ) => Type

在该语法中,new 是关键字,ParameterList 表示可选的构造函数形式参数列表类型,Type 表示构造函数返回值类型。

JavaScript 提供了一个内置的 Error 构造函数,它接受一个可选的 message 作为参数并返回新创建的 Error 对象。示例如下:

const a = new Error();
const b = new Error('Error message.');

我们可以使用如下构造函数类型字面量来表示 Error 构造函数的类型。该构造函数有一个可选参数 message 并返回 Error 类型的对象。示例如下:

let ErrorConstructor: new (message?: string) => Error;

构造签名

构造签名的用法与调用签名类似。若在对象类型中定义了构造签名类型成员,那么我们称该对象类型为构造函数类型。构造签名的语法如下所示:

{
    new (ParameterList): Type
}

在该语法中,new 是运算符关键字,ParameterList 表示构造函数形式参数列表类型,Type 表示构造函数返回值类型,两者都是可选的。

下例中,我们使用对象类型字面量和构造签名定义了一个构造函数类型,该构造函数接受一个 string 类型的参数,并返回新创建的对象:

let Dog: { new (name: string): object };

Dog = class {
    private name: string;
    constructor(name: string) {
        this.name = name;
    }
};

let dog = new Dog('huahua');

此例中,Dog 的类型为构造函数类型,它接受一个 string 类型的参数并返回 object 类型的值。

构造函数类型字面量完全等同于仅包含一个类型成员并且是构造签名类型成员的对象类型字面量。换句话说,构造函数类型字面量是仅包含单个构造签名的对象类型字面量的简写形式,如下所示:

{ new ( ParameterList ): Type }

// 简写为:

new ( ParameterList ) => Type

调用签名与构造签名

有一些函数被设计为既可以作为普通函数使用,同时又可以作为构造函数来使用。例如,JavaScript 内置的 Number() 函数和 String() 函数等都属于这类函数。示例如下:

const a: number = Number(1);

const b: Number = new Number(1);

若在对象类型中同时定义调用签名和构造签名,则能够表示既可以被直接调用,又可以作为构造函数使用的函数类型。示例如下:

{
    new (x: number): Number;  // <- 构造签名
    (x: number): number;      // <- 调用签名
}

此例中,对象类型字面量定义了一个构造签名 new (x: number): Number;,它接受一个 number 类型的参数,并返回 Number 类型的值。同时,该对象类型字面量还定义了一个调用签名 (x: number): number;,它接受一个 number 类型的参数,并返回 number 类型的值。示例如下:

declare const F: {
    new (x: number): Number;  // <- 构造签名
    (x: number): number;      // <- 调用签名
};

// 作为普通函数调用
const a: number = F(1);

// 作为构造函数调用
const b: Number = new F(1);

此例中,函数 F 的类型既是函数类型又是构造函数类型。因此,允许直接调用 F 函数,或者以构造函数的方式调用 F 函数。

重载函数

重载函数是指一个函数同时拥有多个同类的函数签名。例如,一个函数拥有两个及以上的调用签名,或者一个构造函数拥有两个及以上的构造签名。当使用不同数量和类型的参数调用重载函数时,可以执行不同的函数实现代码。

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 代码中不包含函数重载的代码。

函数重载允许存在一个或多个,但只有多于一个的函数重载才有意义,因为若只有一个函数重载,则可以直接定义函数实现。在函数重载中,不允许使用默认参数。函数重载应该位于函数实现(将在下一节中介绍)之前,每一个函数重载中的函数名和函数实现中的函数名必须一致。例如,下例中第 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 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 代码中同样存在。每一个重载函数只允许有一个函数实现,并且它必须位于所有函数重载语句之后,否则将产生编译错误。示例如下:

function add(x: number, y: number): number;
function add(x: any[], y: any[]): any[];

// 函数实现必须位于最后
function add(x: number | any[], y: number | any[]): any {
    // 省略了实现代码
}

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 {
    // 省略了实现代码
}

因此,我们可以使用两个 number 类型的值来调用 add 函数,或者使用两个数组类型的值来调用 add 函数。但是,不允许使用一个 number 类型和一个数组类型的值来调用 add 函数,尽管在函数实现的函数签名中允许这种调用方式。示例如下:

// 正确的调用方式
add(1, 2);
add([1], [2]);

// 错误的调用方式
add(1, [2]);
add([1], 2);

函数实现需要兼容每个函数重载中的函数签名,函数实现的函数签名类型必须能够赋值给函数重载的函数签名类型。示例如下:

function foo(x: number): boolean;
//       ~~~
//       编译错误:重载签名与实现签名的返回值类型不匹配
function foo(x: string): void;
//       ~~~
//       编译错误:重载签名与实现签名的参数类型不匹配
function foo(x: number): void {
    // 省略函数体代码
}

此例中,重载函数 foo 可能的参数类型为 number 类型或 string 类型,同时返回值类型可能为 boolean 类型或 void 类型。因此,在函数实现中的参数 x 必须同时兼容 number 类型和 string 类型,而返回值类型则需要兼容 boolean 类型和 void 类型。我们可以使用联合类型来解决这些问题,示例如下:

function foo(x: number): boolean;
function foo(x: string): void;
function foo(x: number | string): any {
    // 省略函数体代码
}

在其他一些编程语言中允许存在多个函数实现,并且在调用重载函数时编程语言负责选择合适的函数实现执行。在 TypeScript 中,重载函数只存在一个函数实现,开发者需要在这个唯一的函数实现中实现所有函数重载的功能。这就需要开发者自行去检测参数的类型及数量,并根据判断结果去执行不同的操作。示例如下:

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];
    }
}

TypeScript 不支持为不同的函数重载分别定义不同的函数实现。从这点上来看,TypeScript 中的函数重载不是特别便利。

函数重载解析顺序

当程序中调用了一个重载函数时,编译器将首先构建出一个候选函数重载列表。一个函数重载需要满足如下条件才能成为本次函数调用的候选函数重载:

  • 函数实际参数的数量不少于函数重载中定义的必选参数的数量。

  • 函数实际参数的数量不多于函数重载中定义的参数的数量。

  • 每个实际参数的类型能够赋值给函数重载定义中对应形式参数的类型。

候选函数重载列表中的成员将以函数重载的声明顺序作为初始顺序,然后进行简单的排序,将参数类型中包含字面量类型的函数重载排名提前。示例如下:

function f(x: string): void;         // <- 函数重载1
function f(y: 'specialized'): void;  // <- 函数重载2
function f(x: string) {
  // 省略函数体代码
}

f('specialized');  // 7

此例第 7 行,使用字符串参数 'specialized' 调用重载函数 f 时,函数重载 1 和函数重载 2 都满足候选函数重载的条件,因此两者都在候选函数重载列表中。但是因为函数重载 2 的函数签名中包含字面量类型,所以比函数重载 1 的优先级更高。

最终,构造出来的有序候选函数重载列表如下:

  1. 函数重载 2:function f(y: 'specialized'): void;。

  2. 函数重载 1:function f(x: string): void;。

若候选函数重载列表中存在一个或多个函数重载,则使用列表中第一个函数重载。因此,此例中将使用函数重载 2。

如果构建的候选函数重载列表为空列表,则会产生编译错误。例如,当使用 number 类型的参数调用此例中的函数 f 时不存在候选函数重载,因此会产生编译错误,如下所示:

f(1);                         // 编译错误

通过以上的介绍我们能够知道,函数重载的解析顺序依赖于函数重载的声明顺序以及函数签名中是否包含字面量类型。因此,TypeScript 中的函数重载功能可能没有其他一些编程语言那么 智能。这就要求开发者在编写函数重载代码时一定要将最精确的函数重载定义放在最前面,因为它们定义的顺序将影响函数调用签名的选择。示例如下:

function f(x: any): number;   // <- 函数重载1
function f(x: string): 0 | 1; // <- 函数重载2
function f(x: any): any {
    // ...
}

const a: 0 | 1 = f('hi');
//    ~
//    编译错误!类型 'number' 不能赋值给类型 '0 | 1'

此例中,函数重载 2 比函数重载 1 更加精确,但函数重载 2 是在函数重载 1 之后定义的。由于函数重载 2 的参数中不包含字面量类型,因此编译器不会对候选函数重载列表进行重新排序。第 7 行,当使用字符串调用函数 f 时,函数重载 1 位于候选函数重载列表的首位,并被选为最终使用的函数重载。我们能看到 f('hi') 的返回值类型为 number 类型,而不是更精确的 0 | 1 联合类型。若想要修复这个问题,只需将函数重载 1 和函数重载 2 的位置互换即可。示例如下:

function f(x: string): 0 | 1;
function f(x: any): number;
function f(x: any): any {
    // ...
}

const a: 0 | 1 = f('hi');  // 正确

到这里,我们已经介绍了重载函数的大部分功能。因为 TypeScript 语言的自身特点,所以它提供的函数重载功能可能不如其他编程语言那样便利。实际上在很多场景中我们并不需要声明重载函数,尤其是在函数返回值类型不变的情况下。示例如下:

function foo(x: string): boolean;
function foo(x: string, y: number): boolean;
function foo(x: string, y?: number): boolean {
  // ...
}

const a = foo('hello');
const b = foo('hello', 2);


function bar(x: string, y?: number): boolean {
  // ...
}

const c = bar('hello');
const d = bar('hello', 1);

此例中,foo 函数是重载函数,而 bar 函数则为普通函数声明。两个函数在功能上以及可接受的参数类型和函数返回值类型都是相同的。但是,bar 函数的声明代码更少也更加清晰。

重载函数的类型

重载函数的类型可以通过包含多个调用签名的对象类型来表示。例如,有以下重载函数定义:

function f(x: string): 0 | 1;
function f(x: any): number;
function f(x: any): any {
    // ...
}

我们可以使用如下对象类型字面量来表示重载函数 f 的类型。在该对象类型字面量中,定义了两个调用签名类型成员,分别对应于重载函数的两个函数重载。示例如下:

{
    (x: string): 0 | 1;
    (x: any): number;
}

在定义重载函数的类型时,有以下两点需要注意:

  • 函数实现的函数签名不属于重载函数的调用签名之一。

  • 调用签名的书写顺序是有意义的,它决定了函数重载的解析顺序,一定要确保更精确的调用签名位于更靠前的位置。

对象类型字面量以及后面会介绍的接口都能够用来定义重载函数的类型,但是函数类型字面量无法定义重载函数的类型,因为它只能够表示一个调用签名。

小结

本节中,我们主要介绍了重载函数的定义和解析规则,以及如何描述重载函数的类型。细心的读者会发现,我们没有谈及构造函数的重载。实际上构造函数也支持重载并且与本节介绍的重载函数是类似的。关于重载构造函数的详细介绍请参考 5.15.7 节。

函数中 this 值的类型

thisJavaScript 中的关键字,它可以表示调用函数的对象或者实例对象等。本节将介绍函数声明和函数表达式中 this 值的类型。

在默认情况下,编译器会将函数中的 this 值设置为 any 类型,并允许程序在 this 值上执行任意的操作。因为,编译器不会对 any 类型进行类型检查。例如,下例中在 this 值上的所有访问操作都是允许的:

function f() {
    // 以下语句均没有错误
    this.a = true;
    this.b++;
    this.c = () => {};
}

--noImplicitThis

this 值的类型设置为 any 类型对类型检查没有任何帮助。因此,TypeScript 提供了一个 --noImplicitThis 编译选项。当启用了该编译选项时,如果 this 值默认获得了 any 类型,那么将产生编译错误;如果函数体中没有引用 this 值,则没有任何影响。示例如下:

/**
 * --noImplicitThis=true
 */
function f0() {
    this.a = true;     // 编译错误
    this.b++;          // 编译错误
    this.c = () => {}; // 编译错误
}

// 没有错误
function f1() {
    const a = true;
}

函数中 this 值的类型可以通过一个特殊的 this 参数来定义。下面我们将介绍这个特殊的 this 参数。

函数的this参数

TypeScript 支持在函数形式参数列表中定义一个特殊的 this 参数来描述该函数中 this 值的类型。示例如下:

function foo(this: { name: string }) {
    this.name = 'Patrick';

    this.name = 0;
//  ~~~~~~~~~
//  编译错误!类型 0 不能赋值给类型 'string'
}

this 参数固定使用 this 作为参数名。this 参数是一个可选的参数,若存在,则必须作为函数形式参数列表中的第一个参数。this 参数的类型即为函数体中 this 值的类型。this 参数不同于常规的函数形式参数,它只存在于编译阶段,在编译生成的 JavaScript 代码中会被完全删除,在运行时的代码中不存在这个 this 参数。

如果我们想要定义一个纯函数或者是不想让函数代码依赖于 this 的值,那么在这种情况下可以明确地将 this 参数定义为 void 类型。这样做之后,在函数体中就不允许读写 this 的属性和方法。示例如下:

function add(this: void, x: number, y: number) {
    this.name = 'Patrick';
    //   ~~~~
    //   编译错误:属性 'name' 不存在于类型 'void'
}

当调用定义了 this 参数的函数时,若 this 值的实际类型与函数定义中的期望类型不匹配,则会产生编译错误。示例如下:

function foo(this: { bar: string }, baz: number) {
    // ...
}

// 编译错误
// 'this'类型为'void',不能赋值给 '{ bar: string }' 类型的this
foo(0);

foo.call({ bar: 'hello' }, 0); // 正确

此例第 1 行,将 foo 函数 this 值的类型设置为对象类型 { bar: string }。第 7 行,调用 foo 函数时 this 值的类型为 void 类型,它与期望的类型不匹配,因此产生编译错误。第 9 行,在调用 foo 函数时指定了 this 值为 { bar: 'hello' },其类型符合 this 参数的类型定义,因此不会产生错误。Function.prototype.call() 方法是 JavaScript 内置的方法,它能够指定调用函数时使用的 this 值。