TypeScript入门到精通——TypeScript类型系统基础——函数类型
函数类型
一、常规参数类型
在函数形式参数列表中,为参数添加类型注解就能够定义参数的类型。例如,在下列中将 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){ return x + y; }
二、可选参数类型
在JavaScript中,函数的每一个参数都是可选参数,而在TypeScript中,默认情况下函数的每一个参数都是必选参数。
JavaScript 示例如下:
function greet(name) { console.log("Hello, " + name); }
当调用greet(John)
,这个函数会正常运行,因为name
参数是可选的。如果传递的参数比函数定义中需要的参数少,那么未被传递的参数会被设置为undefined
。
在TypeScript中,所有参数默认都是必需的。如果尝试调用一个函数而没有提供所有参数,TypeScript会抛出一个错误。例如,如果在TypeScript中定义了一个函数:
function greet(name: string) { console.log("Hello, " + name); }
调用greet(John)
,TypeScript会抛出一个错误,因为没有提供所有必需的参数。如果希望某些参数为可选的,需要明确地在函数定义中指出。这可以通过在参数名前加上问号(?
) 来实现,如下所示:
function greet(name?: string) { console.log("Hello, " + name); }
在这个例子中,name
参数就是可选的。现在可以调用greet(John)
而不会得到任何错误。
三、默认参数类型
函数默认参数类型可以通过类型注解定义,也可以根据默认参数值自动地推断类型。
例如,如果我们有一个函数,它接受两个参数,其中第二个参数有一个默认值,我们可以使用类型注解来定义这个参数的类型:
function greet(name: string, punctuation: string = '!') { return `${name}${punctuation}`; } console.log(greet('Alice')); // 'Alice!' console.log(greet('Bob', '??')); // 'Bob??'
在这个例子中,punctuation
参数有一个默认值'!'
,并且它的类型被注解为string
。
function greet(name: string, punctuation = '!') { return `${name}${punctuation}`; } console.log(greet('Alice')); // 'Alice!' console.log(greet('Bob', '??')); // 'Bob??'
如果我们试图给punctuation
参数传递一个非字符串的值,TypeScript将会发出警告,如下:
四、剩余参数类型
必选参数、可选参数和默认参数处理的都是单个参数,而剩余参数处理的则是多个参数。如果函数定义中声明了剩余参数,那么在调用函数时会将多余的实际参数收集到剩余参数列表中。因此,剩余参数的类型应该为数组类型或元组类型。
4.1、数组类型的剩余参数
最常见的做法是将剩余参数的类型声明为数组类型。例如:
function f(...args: number[]){}
在调用定义了剩余参数的函数时,剩余参数可以接受零个或多个实际参数。
fucntion f(...args: number[]){}
4.2、元组类型的剩余参数
剩余参数的类型也可以定义为元组类型。
function f(...args:[boolean,number]){}
如果剩余参数的类型为元组类型,那么编译器会将剩余参数展开为独立的形式参数声明。
4.2.1、常规元组类型
function f0(...args: [boolean, number]) () //等同于 function f1(args_0:boolean, args_1: number){}
4.2.2、带有可选元素的元组类型
fucntion f0(...args: [boolean, string?]){} // 等同于 function f1(args_0: boolean, args_1?: string){}
4.2.3、带有剩余元素的元组类型
function f0(...args: [boolean, ...string[]]){} // 等同于 function f1(args_0: boolean, ...args_1: string[]) {}
五、解构参数类型
这里提到的"解构"是指在 JavaScript (以及 TypeScript) 中,可以将一个数组或者对象赋值给一个变量,而这个变量将自动提取数组或对象的属性并存储它们。这被称为"解构赋值"。
解构可以应用在函数参数列表中,例如:
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; }
在 TypeScript 的原始类型里有一个特殊的空类型 void,该类型唯一有意义的使用场景就是作为函数的返回值类型。如果一个函数的返回值类型为 void,那么这函数只能返回 undefined 值。
function doSomething(): void { // 这个函数不会返回任何值 } function doSomethingElse(): string { return "Hello, World!"; // 这个函数会返回一个字符串 }
在第一个函数 doSomething 中,我们声明了一个返回类型为 void 的函数。这意味着该函数不会返回任何值。在函数体中,我们可以执行任何操作,例如打印日志、修改状态等,但不能使用 return 语句返回任何值。
在第二个函数 doSomethingElse 中,我们声明了一个返回类型为 string 的函数。这意味着该函数必须返回一个字符串值。我们可以使用 return 语句返回一个字符串,并且该返回值将被赋予函数的调用者。
需要注意的是,如果一个函数的返回类型为 void,那么该函数只能返回 undefined 值。在 TypeScript 中,使用 void 类型作为函数的返回类型是不常见的,因为这种用法通常是不必要的。如果一个函数不需要返回任何值,我们通常会省略返回类型或者使用 void 类型作为返回类型,但实际上该函数仍然会返回 undefined 值。
七、函数类型字面量
函数类型字面量是定义函数类型的方法之一,它能够制定函数的参数类型、返回值类型。函数类型字面量的语法与箭头函数的语法相似,具体语法如下所示:
(ParameterList) => Type
举个基本例子:
let add: (number, number) => number = (a, b) => a + b;
在这个例子中,add
被定义为一个接受两个数字参数(number
),并返回一个数字(number
)的函数。等号右边是一个箭头函数,它实现了这个功能。
此外,函数类型字面量还可以有可选参数、具名参数、默认参数等。例如:
let add: (number, number?) => number = (a, b = 0) => a + (b || 0);
在这个例子中,add
是一个接受一个或两个数字参数的函数,并返回一个数字。第二个参数是可选的,如果没有提供,则默认为 0。
八、调用签名
函数在本质上是一个对象,但特殊的地方在于函数是可调用的对象。
因此,可以使用对象类型来表示函数类型。若在对象类型中定义了调用签名类型成员,那么我们称该对象类型为函数类型。调用签名的语法如下所示:
{ (ParameterList): Type }
在该语法中,ParameterList 表示函数形式参数列表类型,Type 表示函数返回值类型,两者都是可选的。
下例中,我们使用对象类型字面量和调用签名定义了一个函数类型,该函数类型接受两个 number 类型的参数,并返回 number 类型的值:
let add: { (x: number, y: number): number}; add = function (x: number, y: number): number { return x + y; }
九、构造函数类型字面量
在面向对象编程中,构造函数是一类特殊的函数,它用来创建和初始化对象。JavaScript中的函数可以作为构造函数使用,在调用构造函数时需要使用 new 运算符。
例如,我们可以使用内置的 Date 构造函数来创建一个日期对象,示例如下:
const date = new Date(); console.log(date); // 输出当前的日期和时间
在TypeScript中,new Date()
创建了一个新的Date对象。Date对象代表了一个特定的时间点。当你执行new Date()
时,TypeScript会执行以下步骤:
- 分配内存:TypeScript 在内存中为新的 Date 对象分配空间。这个空间足够存储 Date 对象的数据和属性。
- 初始化属性:TypeScript 会设置 Date 对象的属性,包括年、月、日、小时、分钟、秒和毫秒等。这些属性是根据当前的系统时间设置的。
- 返回对象:最后,TypeScript 返回新创建的 Date 对象。你可以将这个对象赋值给一个变量,或者直接使用它。
构造函数类型字面量是定义构造函数类型的方法之一,它能够制定构造函数的参数类型、返回值类型。构造函数类型字面量的具体语法如下所示:
new (ParameterList) => Type
new 是关键字,ParameterList 表示可选的构造函数形式参数列表类型,Type 表示构造函数返回值类型。
十、构造签名
构造签名的用法与调用签名类似。若在对象类型中定义了构造签名类型成员,那么我们称该对象类型为构造函数类型。构造签名的语法如下所示:
{ 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')
这段 TypeScript 代码定义了一个类 Dog,以及一个类型 Dog。让我们逐行解析这段代码:
1、let Dog: { new (name: string): object};
这行代码定义了一个名为 Dog 的类型,这个类型是一个构造函数,它接受一个名为 name 的字符串参数,并返回一个 object 类型的实例。
2、Dog = class { ... };
这行代码定义了一个实际的 Dog 类。这个类有一个私有属性 name 和一个构造函数。
3、private name: string;
这行代码定义了一个私有属性 name,它的类型是 string。这意味着每只 Dog 对象都会有一个 name 属性,并且这个属性只能被 Dog 类的其他方法访问。
4、constructor(name: string) { this.name = name; }
这行代码定义了 Dog 类的构造函数。构造函数是一个特殊的方法,当创建类的实例时会被调用。这个构造函数接受一个字符串参数 name,并将这个值赋给 this.name。这里 this 关键字指的是当前对象的实例。
5、let dog = new Dog('huahua');
这行代码创建了一个新的 Dog 对象,并给它 name 属性赋值为 'huahua'。然后这个新创建的 Dog 对象被赋值给变量 dog。
所以,这段代码执行后的结果是:我们创建了一个名为 dog 的变量,它是一个新的 Dog 对象,这个对象的 name 属性值为 'huahua'。
十一、调用签名与构造签名
有一些函数被设计为既可以作为普通函数使用,同时又可以作为构造函数来使用。例如,JavaScript 内置的 "Number()" 函数和 "String()" 函数等都是属于这类函数。
11.1、调用签名(Call Signature)
定义:调用签名定义了函数的名字,参数列表以及返回值类型。我们可以通过调用签名来定义函数的行为。例如:
function add(x: number, y: number): number { return x + y; }
在这个例子中,add
是函数的名字,(x: number, y: number)
是参数列表,: number
是返回值类型。这个函数的行为非常简单,就是接收两个数字参数,然后返回它们的和。
11.2、构造签名(Construct Signature)
定义:构造签名定义了类的构造函数的行为。构造签名可以有一个参数列表和任意数量的类型参数。例如:
class Rectangle { width: number; height: number; constructor(width: number, height: number) { this.width = width; this.height = height; } }
在这个例子中,Rectangle 是一个类,constructor 是它的构造函数。这个构造函数接收两个参数(width 和 height),并使用它们来初始化 Rectangle 类的 width 和 height 属性。
这两种签名在 TypeScript 中都是非常重要的,因为它们可以帮助我们确保代码的类型安全和正确性。如果我们尝试使用错误的参数类型或者返回值类型调用函数,TypeScript 编译器就会给出错误提示。同样,如果我们尝试实例化一个类但是提供的参数类型不匹配构造函数的参数类型,TypeScript 也会给出错误提示。
十二、重载函数
重载函数 是指一个函数同时拥有多个同类的函数签名。
例如,一个函数拥有两个及以上的签名,或者一个构造函数拥有两个及以上的构造签名。当使用不同数量和类型的参数调用重载函数时,可以执行不同的函数实现代码。
TypeScript 中的重载函数与其他编程语言中的重载函数略有不同,主要区别在于TypeScript的重载函数是基于静态类型的,而其他语言则是在运行时进行动态类型检查。
这意味着在 TypeScript 中,重载函数的类型检查是在编译时进行的,而不是在运行时。这使得 TypeScript 的重载函数更加严格和类型安全。如果调用了一个参数类型不匹配的重载函数,TypeScript 编译器会在编译时给出错误提示,而不是在运行时抛出异常。
相比之下,其他主流编程语言(如 Java、C#、JavaScript 等)的重载函数是在运行时进行类型检查的。这意味着即使在编译时通过了类型检查,也可能会在运行时因为实际参数类型的不同而抛出异常。因此,这些语言的重载函数需要更多的运行时类型检查和异常处理代码来确保类型安全。
此外,TypeScript 的重载函数还支持可变参数和默认参数,这使得能够更加灵活地处理不同数量和类型的参数。在 TypeScript 中,可以通过使用 rest 参数来处理可变数量的参数,或者使用默认参数来指定默认值。这些特性在其他主流编程语言中也是存在的,但是 TypeScript 的语法更加简洁明了。
12.1、函数重载
不带有函数体的函数声明语句叫作函数重载。例如,下例中的 add 函数声明没有函数体,因此它属于函数重载:
function add(x: number, y: number): number;
函数重载的语法中不包含函数体,它只提供了函数的类型信息。函数重载只存在于代码编译阶段,在编译生成 JavaScript 代码时会被完全删除,因此在最终生成的 JavaScript 代码终不包含函数重载的代码。
函数重载允许存在一个或多个,但只有多余一个的函数重载才有意义,因为若只有一个函数重载,则可以直接定义函数实现。
在函数重载中,不允许使用默认参数。函数重载应该位于函数实现之前,每一个函数重载中的函数名和函数实现中的函数名必须一致。
例如,你可能会有如下的 TypeScript 代码:
function foo(x: number): void; function foo(x: string): void; function foo(x: number | string): void { // 实现代码 }
在生成的 JavaScript 代码中,上述代码将被转换为:
function foo(x) { if (typeof x === 'number') { // 实现代码 for number } else if (typeof x === 'string') { // 实现代码 for string } }
这样,函数 foo 就可以根据传入的参数类型执行不同的操作。
需要注意的是,虽然 TypeScript 支持函数重载,但并不支持运算符重载。另外,如果函数重载数量只有一个,那么实际上没有必要使用重载,因为你可以直接在函数内部进行类型检查和分支。
此外,虽然 TypeScript 不允许在函数重载中使用默认参数,但你可以在函数实现中使用默认参数。例如:
function foo(x: number, y?: string): void; // 重载声明 function foo(x: number, y?: string): void { // 实现代码 if (y === undefined) { y = 'default'; // 使用默认参数 } // 实现代码 }
这样的代码是合法的,因为 TypeScript 的类型系统将函数的重载声明和实现代码视为不同的实体。
12.2、函数实现
函数实现包含了实际的函数体代码,该代码不仅在编译时存在,在编译生成的 JavaScript 代码中同样存在。每一个重载函数只允许有一个函数实现,并且它必须位于所有函数重载语句之后,否则将产生编译错误。
function combine(input1: number, input2: number): number; function combine(input1: string, input2: string): string; // 实现 function combine(input1: any, input2: any): any { if (typeof input1 === "number" && typeof input2 === "number") { return input1 + input2; } if (typeof input1 === "string" && typeof input2 === "string") { return input1.concat(input2); } return null; }
在这个例子中,combine 函数有两个重载,一个接受两个数字参数并返回一个数字,另一个接受两个字符串参数并返回一个字符串。然后我们有一个实现函数,它接受任何类型的参数并返回 null。这个实现函数必须位于所有重载函数之后,否则 TypeScript 编译器将无法找到与特定函数调用匹配的重载。
12.3、函数重载解析顺序
当程序中调用了一个重载函数时,编译器将首先构建出一个候选函数重载列表。一个函数重载需要满足如下条件才能成为本次函数调用的候选函数重载:
- 参数类型必须与函数重载的参数类型相匹配。这是通过类型比较来完成的,编译器会检查每个调用参数的类型是否与某个重载函数的参数类型相匹配。
- 参数数量必须与函数重载的参数数量相同或更少。如果调用参数的数量比函数重载的参数数量多,编译器就无法选择一个合适的重载函数。
- 重载函数必须在同一个作用域内定义。如果函数是在不同的作用域中定义的,那么编译器就无法选择一个合适的重载函数。
- 如果一个函数重载使用了可选参数,那么在调用该函数时,必须提供与可选参数数量相同的参数。这是为了确保函数调用是有效的。
- 如果一个函数重载使用了默认参数,那么在调用该函数时,可以不提供该参数。这是为了方便程序员编写代码。
- 如果一个函数重载使用了 rest 参数,那么在调用该函数时,必须提供与 rest 参数数量相同的参数。这是为了确保函数调用是有效的。
- 对于一个带有可选参数或默认参数的函数重载,如果某个参数的类型是 any 或 Function,那么在调用该函数时,可以不提供该参数。这是为了方便程序员编写代码。
12.4、重载函数的类型
重载函数的类型可以通过包含多个调用签名的对象类型来表示。例如,有以下重载函数定义:
function f(x: string): 0 | 1; function f(x: any): number; function f(x: any): any{ // ... }
我们可以使用如下对象类型字面量来表示重载函数 f 的类型。在该对象类型字面量中,定义了两个调用签名类型成员,分别对应于重载函数的两个函数重载。
{ (x: string): 0 | 1; (x: any): number; }
十三、函数中 this 值
this 是 JavaScript 中的关键字,它可以表示调用函数的对象或者实例对象等。
在默认情况下,编译器会将函数中的 this 值设置为 any 类型,并允许程序在 this 值上执行任意的操作。因为编译器不会对 any 类型进行类型检查。
TypeScript 支持在函数形式参数列表中定义一个特殊的 this 参数来描述该函数中 this 值的类型。示例如下:
function foo(this: {name: string}) { this.name = 'PPAP'; this.name = 0; }
报错:
当调用定量 this 参数的函数时,若 this 值的实际类型与函数定义中的期望类型不匹配,则会产生编译错误。