在上一节中,我们学习了可选参数和默认参数,解决了函数参数 “传不传” 的问题。但在实际的业务开发中,我们还会遇到另一种令人头疼的场景:我们根本不知道调用者会传几个参数进来。
举个简单例子,我们想封装一个计算多个数字总和的数学函数,用户可能传 2 个数字,也可能传 5 个,甚至 100 个。
在以前使用原生 JavaScript 的时代,老鸟们可能会用 arguments 对象来获取这些参数,但这玩意儿并不是一个真正数组,而是一个 “伪数组”,并没有 reduce() 等常用方法。像这种 “伪数组” 操作起来是非常难受的,而且在 TypeScript 中毫无类型提示可言。
为了优雅地解决这种 “不定个数参数” 的类型约束问题,TypeScript 全面拥抱了 ES6 的新特性:剩余参数(Rest Parameters)。
TypeScript 剩余参数是什么?
在 TypeScript 中,剩余参数允许我们将一个不确定数量的散装参数,自动收集到一个“真正数组” 中。
语法:
const 函数名 = (...参数名: 类型[]) => {
// 函数体
};说明:
剩余数组的语法非常直观,就是在参数名前面加上三个点 “...”。
既然收集起来的是一个数组,那么在 TypeScript 中,我们只需要给这个参数指定为一个数组类型(比如 number[] 或 string[])就可以了。
示例 1:使用剩余参数求和
// 定义函数,使用剩余参数
const sum = (...nums: number[]): number => {
return nums.reduce((total, current) => total + current, 0);
};
// 随便传多少个数字都可以
const result1: number = sum(1, 2, 3);
const result2: number = sum(10, 20, 30, 40, 50);
console.log("结果 A:", result1);
console.log("结果 B:", result2);运行结果如下。
结果 A:6
结果 B:150分析:
在这个例子中,我们使用剩余参数 “...nums: number[]”,用于明确地告诉 TypeScript:“请把用户传进来的所有参数统统打包,塞进 nums 这个数组里,并且这个数组里面的每一个元素都必须是数字。”
这样一来,不管用户传入多少个参数,我们都能安全优雅地进行数组操作。
TypeScript 剩余参数与常规参数混用
在真实的企业级业务场景下(比如封装后端的日志上报系统),我们往往需要将 “固定参数” 和 “剩余参数” 混合起来使用。
比如,第一个参数固定是 “日志级别(level)”,后面的参数则是不定长的 “具体日志内容”。
示例 2:混合使用参数
const printLog = (level: string, ...messages: string[]): void => {
console.log(`[${level}]:`, messages.join(" - "));
};
printLog("ERROR", "网络请求超时", "500 错误");
printLog("INFO", "用户登录成功");运行结果如下。
[ERROR]:网络请求超时 - 500 错误
[INFO]:用户登录成功分析:
在这个例子中,第 1 个参数 level 是固定的,后面所有的参数都被收集到 messages 数组中。
与上一节提到的 “可选参数(?)” 非常类似,剩余参数也有一条绝对不可触碰的底线:剩余参数必须且只能出现在参数列表的最后一位。
如果我们试图把剩余参数放在前面或中间,那么 TypeScript 编译器会直接报错拦截:
// 报错:剩余参数必须是参数列表中的最后一个参数。
const badFunc = (...args: string[], lastParam: number) => { ... }这个设定这在逻辑上也是合理的:如果你把一个可以无限吞噬参数的 “无底洞” 放在前面,那么 TypeScript 编译器怎么知道哪里才是它的尽头,从而把后面的参数分给别人呢?对吧?
TypeScript 使用 “元组” 约束剩余参数
如果你是一个追求极致类型安全的极客,可能会遇到这样的变态需求:“虽然我使用了剩余参数,但我确切地知道用户传进来的数据结构——第 1 个参数必须是字符串,第 2 个参数必须是数字”。
对于这种具有 “固定类型排列” 的不定参数,我们可以结合前面学过的 “元组” 来进行死磕约束。
示例 3:剩余参数配合元组(实现参数复用)
// 1. 将参数结构抽离成一个独立的元组类型
type UserInfoArgs = [string, number];
// 2. 在函数的剩余参数中,直接复用该类型
const testTupleRest = (...args: UserInfoArgs): void => {
console.log("姓名:", args[0]);
console.log("年龄:", args[1]);
};
testTupleRest("Jack", 20);运行结果如下。
姓名:Jack
年龄:20分析:
很多小伙伴可能会问:“既然明确知道是字符串和数字,为什么不直接写成 (name: string, age: number) 呢?”
实际上,如果我们直接在函数里硬编码写成 (...args: [string, number]) 确实显得多此一举。但结合上一章学过的类型别名(type),我们将参数结构抽离为了 UserInfoArgs。
设想一下,当你的大型项目中有十几个函数都需要接收 “姓名 + 年龄” 的参数组合时,使用这种写法能实现参数类型声明的极致复用!如果以后业务变更,不仅需要传姓名和年龄,还需要传 “性别”,我们只需要修改 UserInfoArgs 这一个地方,所有函数的参数校验规则都会自动同步。
TypeScript 剩余参数与展开运算符
前面我们介绍的都是在 “定义函数” 时,使用 “...” 把散装的参数收集成一个数组。
反过来,如果我们在调用函数时,手里刚好已经有了一个完整的数组,然后想要把它打散传给剩余参数,应该怎么做呢?
实际上,我们同样可以使用 “...”,这在语法上被称为 “展开运算符”。它能完美地把数组里的元素剥离出来,逐个 “喂” 给函数的剩余参数。
提示: 展开运算符并不是 TypeScript 特有的,而是 ES6 新增的语法。
示例 4:使用展开运算符
// 定义函数
const sum = (...nums: number[]): number => {
return nums.reduce((total, current) => total + current, 0);
};
// 定义一个纯数字数组
const myArr: number[] = [10, 20, 30];
// 使用 “...” 将数组打散后传入
const total: number = sum(...myArr);
console.log(total);运行结果如下。
60分析:
在调用 sum(...myArr) 时,TypeScript 编译器会非常严谨地去检查 myArr 的类型。如果 myArr 里面混入了字符串,或者它本身不是数组,TypeScript 都会直接报错拦截。
