大家好,我是半虹,这篇文章来讲 TypeScript 中的函数类型
1、函数定义
在 JavaScript 中,定义函数有两种方式,分别是函数声明和函数表达式
其中函数表达式又可以细分为两种情况,分别是普通函数和箭头函数,对应例子如下:
// 函数声明
function add1(a, b) {
return a + b;
}
// 函数表达式:普通函数
let add2 = function(a, b) {
return a + b;
}
// 函数表达式:箭头函数
let add3 = (a, b) => {
return a + b;
}
在 TypeScript 中,同样支持这些方式,并且允许给函数参数和返回值显式声明类型
只需要在函数字面量直接指定即可,对应例子如下:
// 函数声明
// 格式如下:function name(para1: type1, ...):return_type { ... }
function add1(a:number, b:number):number {
return a + b;
}
// 函数表达式:普通函数
// 格式如下:let name = function(para1: type1, ...):return_type { ... }
let add2 = function(a:number, b:number):number {
return a + b;
}
// 函数表达式:箭头函数
// 格式如下:let name = (para1: type1, ...):return_type => { ... }
let add3 = (a:number, b:number):number => {
return a + b;
}
如果没有显式指定参数类型,那么也会做隐式推导
但是绝大多数情况由于缺少上下文信息,因此难以推断出正确的类型,此时会看作是 any
与之相反的是,返回值类型通常能根据上下文信息推断,因此可以省略不写
但是有的时候,为了函数签名更加清晰,或者防止返回值被无意修改,也能显式指定
// 省略参数的类型和返回值类型,由于此时缺少必要的上下文信息,因此都会被推断为 any
let add1 = function(x, y) {
return x + y;
}
// 省略返回值类型,此时还能根据参数的类型和函数的操作,正确推断出返回值类型为 number
let add2 = function(x:number, y:number) {
return x + y;
}
使用函数表达式定义函数时,可以注意到我们没有给等号左边的变量显式地声明类型
实际上这里也是隐式推导嘞,通过等号右边的值的类型自动推导等号左边的变量类型
这就跟我们之前定义值变量时是一样的,只不过之前的值是数字等简单类型,而这里的值是函数
区别是函数作为值时,可以在函数字面量中指定参数和返回值类型
// 数字变量
// 省略左值类型,由右值自动推导,这里右值是数字
// 最后左值类型推断为:number
let x1 = 1;
x1 = 123; // 赋值类型与推导类型相同,编译正常
x1 = '0'; // 赋值类型与推导类型不同,编译错误
// 函数变量
// 省略左值类型,由右值自动推导,这里右值是函数,可以在函数字面量指定参数和返回值类型
// 最后左值类型推断为:(x:number, y:number) => number
let x2 = function(x:number, y:number):number {
return x + y;
}
x2 = function(x:number, y:number):number { return x - y; } // 赋值类型与推导类型相同,编译正常
x2 = function(x:string, y:string):string { return x + y; } // 赋值类型与推导类型不同,编译错误
实际上我们可以通过类型签名显式指定变量类型,注意,类型签名中只有类型没有值
其语法就和箭头函数一样,箭头的左侧指定参数的类型,箭头的右侧指定返回值类型
// 类型签名
// 签名意为:接收两个数字作为参数,返回一个数字
type X1 = (x:number, y:number) => number;
// 变量指定类型
let x1:X1;
// 变量赋值
// 赋值类型需要与类型签名相同
X1 = function(x:number, y:number):number { return x + y; } // 赋值类型与类型签名相同,编译正常
x1 = function(x:number, y:number):number { return x - y; } // 赋值类型与类型签名相同,编译正常
x1 = function(x:string, y:string):string { return x + y; } // 赋值类型与类型签名不同,编译错误
通过类型签名显式声明变量类型后,赋值需要与类型签名一致,并有以下的注意事项:
赋值时的参数名称可以和类型签名中的参数名称不一致,此时没有任何影响
赋值时的参数类型可以被省略,此时会根据类型签名自动推导出正确的类型
赋值时的参数数量可以比类型签名中的参数数量要更少,但是不能比它更多
// 类型签名
// 签名意为:接收两个数字作为参数,返回一个数字
type X1 = (x:number, y:number) => number;
// 变量指定类型
let x1:X1;
// 变量赋值
x1 = function(a:number, b:number):number { return a + b; } // 赋值时参数名称与类型签名不同,编译正常
x1 = function(a, b) { return a + b; } // 赋值时参数类型省略并自动推导,编译正常
x1 = function(a:number) { return a; } // 赋值时参数数量比类型签名更少,编译正常
x1 = function(a:number, b:number, c:number):number { return a + b + c; } // 赋值时参数数量比类型签名更多,编译错误
函数调用时,实参数量与形参数量必须相等,既不能多,也不能少
let f:(x:number, y:number) => number = (x, y) => (x + y);
f(1, 2); // 实参数量与形参数量相等,编译正常
f(1); // 实参数量比形参数量要少,编译错误
f(1, 2, 3); // 实参数量比形参数量要多,编译错误
2、可选参数
在有些时候,实参数量可能比形参数量更少,这时就要用可选参数
可选参数表示某个参数在调用之时可以省略,此时相当于显式赋值 undefined
可选参数可以有多个,但必须位于参数最后,只需在对应参数后面加问号即可
// 定义
let log0 = function(x?:number, y:number) { // 第一个参数可选,编译错误,可选参数只能在必选参数后
console.log(x, y);
}
let log1 = function(x:number, y?:number) { // 第二个参数可选,编译正常
console.log(x, y);
}
let log2 = function(x?:number, y?:number) { // 第一个和第二个参数都可选,编译正常
console.log(x, y);
}
// 调用
log1(1); // 传入一个参数,相当于 log1(1, undefined)
log1(1, 2); // 传入两个参数
log2(); // 传入零个参数,相当于 log2(undefined) 或 log2(undefined, undefined)
log2(1); // 传入一个参数(第一个参数)相当于 log2(1, undefined)
log2(undefined, 2); // 传入一个参数(第二个参数)此时第一个参数只能显式赋值为 undefined
log2(1, 2); // 传入两个参数
3、剩余参数
在有些时候,实参数量可能比形参数量更多,这时就要用剩余参数
剩余参数表示可以接收一组声明的所有参数,既可以是类型相同的数组,也可以是类型不同的元组
剩余参数只能有一个,且必须位于参数最后,只需在对应参数位置用拓展运算符展开数组或者元组
// 定义
let log0 = function(...rest:number[], x:number) { // 编译错误,剩余参数必须位于参数最后
console.log(x, rest);
}
let log1 = function(x:number, ...rest:number[]) { // 编译正常,使用拓展运算符来展开数组
console.log(x, rest);
}
let log2 = function(x:number, ...rest:[number, string, boolean]) { // 编译正常,使用拓展运算符来展开元组
console.log(x, rest);
}
// 调用
log1(1); // 剩余参数传零个值,此时 rest 为 []
log1(1, 2); // 剩余参数传一个值,此时 rest 为 [2]
log1(1, 2, 3); // 剩余参数传两个值,此时 rest 为 [2, 3]
// ...
log2(1, 123, '0', true); // 此时 rest 为 [123, '0', true]
4、默认参数
就像在 JavaScript 中一样,我们可以给函数参数指定默认值
默认值只能在赋值时函数字面量指定,不能使用类型签名,因为类型签名只有类型没有值
默认值可以在任意的位置,只需在对应参数后用等号赋予值
带有默认值的参数既能显式指定类型,同时也能省略不写,此时就会根据默认值自动推断
最后需要注意的是,默认值和可选参数不能用于同一个参数
// 不能在一个参数上同时用默认值和可选
let repeat0 = function(str:string, num?:number = 10) { // 编译错误
console.log(str.repeat(num));
}
// 第二个参数设置默认值,并且省略类型,此时根据默认值能推断出是 number
let repeat1 = function(str:string, num = 10) {
console.log(str.repeat(num));
}
// 第一个参数设置默认值,并且省略类型,此时根据默认值能推断出是 string
let repeat2 = function(str = '', num:number) {
console.log(str.repeat(num));
}
repeat1('s'); // 默认参数没有传入值,此时就是设置的默认值
repeat1('s', undefined); // 默认参数传入 undefiend,此时也是默认值
repeat1('s', 100); // 默认参数传入合法值,此时就是传入值
repeat2(undefined, 100); // 默认参数不在最后的位置不能省,此时可以传 undefined 使用默认值
repeat2('s', 100); // 默认参数不在最后的位置不能省,或者可以传 合法值,此时就是传入值
5、只读参数
如果希望函数内部不能修改某个参数,可以将该参数设置为只读参数
只需在该参数前加 readonly
,但是要注意嘞,只能用于数组或元组
let modify0 = function(num:readonly number) { // 编译错误
num = 1;
}
let modify1 = function(num:readonly number[]) { // 编译正常
num[0] = 1; // 编译错误
}
let modify2 = function(num:readonly [number]) { // 编译正常
num[0] = 1; // 编译错误
}
6、隐式参数
函数在调用时,有两个参数无需声明,就会自动传入,因此被称为隐式参数
分别是 arguments
和 this
,其中 arguments
可以获取传入的所有参数
而 this
则是当前上下文对象,具体的值由调用时的上下文决定,难以确定
let a = {
b() {
console.log(this);
}
}
let b = a.b;
a.b(); // 此时 this 为 a
b(); // 此时 this 为 全局对象
TypeScript 可以通过显式声明 this
的类型来确保 this
处于预期的上下文
需要注意的是, this
的声明必须位于参数最前面,并对调用传参没有影响
// 格式化日期,调用时要求 this 为 Date
let formatDate = function(this: Date, info: string) {
return `[${info}] ${this.getFullYear()}/${this.getMonth()}/${this.getDate()}`;
}
formatDate.call(new Date, 'call'); // 编译正常
formatDate('direct'); // 编译错误
7、函数重载
所谓函数重载,就是说一个函数有多个类型签名,调用时会根据实际的参数执行不同的行为
前面我们介绍的类型签名其实是简写版本,如果想定义重载函数,可以使用完整版类型签名
// 简写版类型签名,类似于箭头函数
// 箭头的左侧表示参数及其类型,箭头的右侧表示返回类型
type X1 = (x:number, y:number) => number;
// 完整版类型签名,类似于对象字面量
// 每一个属性代表一个类型签名,属性之间用逗号或分号分隔
// 属性中的键表示参数及其类型,属性中的值表示返回类型,键值之间用冒号分隔
type X2 = {
(x:number, y:number): number;
// ...
}
// 声明多个签名时,顺序很重要
// 因为调用函数时,会根据顺序进行匹配,一旦匹配符合后,那么就不再进行检查
// 所以宽泛的签名应该放在后面
开始声明类型时,可以通过完整版的类型签名逐一声明多条签名
但是实现函数时,则必须要同时实现所有签名,这里要注意的是:
- 参数的类型声明必须能包含所有可能情况
- 函数的处理逻辑必须要同时处理所有情况
最后调用函数时,传入参数需要符合任一签名
// 声明类型,定义加法函数,重载逻辑如下:
// 如果传入两个数字,那么将两个数字相加并返回
// 如果传入一个数组,那么将所有数字相加并返回
type AddT = {
(x:number, y:number): number;
(x:number[]): number;
}
// 实现函数
let addF:AddT = function(
x: number|number[], // 如果是签名 1,那么 x 是 number;如果是签名 2,那么 x 是 number[]
y?: number // 如果是签名 1,那么 y 是 number;如果是签名 2,那么 y 为 空,所以该参数为可选
):number {
// 处理签名 1 的情况
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
}
// 处理签名 2 的情况
if (Array.isArray(x)) {
return x.reduce(function(prev, curr) { return prev + curr; })
}
// 兜底
throw new Error('wrong parameters');
}
// 调用函数
addF(1); // 编译错误,单看函数字面量没问题,x 是 number|number[], y 是可选;但是该传参不符合任何一条签名
addF(1, 2); // 编译正常,符合签名 1
addF([1, 2]); // 编译正常,符合签名 2
上面介绍的是函数表达式的重载,实际上函数声明也能定义重载
function addF(x:number, y:number):number; // 签名 1
function addF(x:number[]):number; // 签名 2
function addF( // 实现
x: number|number[],
y?: number,
) {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
}
if (Array.isArray(x)) {
return x.reduce(function(prev, curr) { return prev + curr; })
}
throw new Error('wrong parameters');
}
好啦,本文到此结束,感谢您的阅读!
如果你觉得这篇文章有需要修改完善的地方,欢迎在评论区留下你宝贵的意见或者建议
如果你觉得这篇文章还不错的话,欢迎点赞、收藏、关注,你的支持是对我最大的鼓励 (/ω\)