TypeScript 剩余参数

在上一节中,我们学习了可选参数和默认参数,解决了函数参数 “传不传” 的问题。但在实际的业务开发中,我们还会遇到另一种令人头疼的场景:我们根本不知道调用者会传几个参数进来。

举个简单例子,我们想封装一个计算多个数字总和的数学函数,用户可能传 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 都会直接报错拦截。

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号