TypeScript 可选参数与默认参数

默认情况下,TypeScript 对函数参数数量的检查是非常严苛的:定义了多少个参数,调用时就必须老老实实传多少个参数,多一个会报错,少一个也会报错。

但在实际开发中,业务逻辑往往是没有这么死板的。比如我们封装一个 “构建用户全名” 的函数,用户的姓氏(firstName)是必填的,但名字(lastName)可能由于各种原因暂时没有提供。

面对这种 “可传可不传” 的参数需求,TypeScript 为我们提供了两种解决方案:可选参数和默认参数。

TypeScript 函数的可选参数 (?)

在 TypeScript 中,如果想让函数的某个参数变成 “可选” 的,我们只需要在该参数名的后面加上一个问号 “?” 即可。

语法:

// 传统函数声明
function 函数名(必选参数: 类型, 可选参数?: 类型): 返回值类型 {
    // 函数体逻辑
}

// 箭头函数声明
const 函数名 = (必选参数: 类型, 可选参数?: 类型): 返回值类型 => {
    // 函数体逻辑
};

说明:

到目前为止,关于 “可选” 这种操作,其实我们已经接触过 3 次了。小伙伴们可以对比理解一下。

  • 元组的可选元素。
  • 接口的可选属性。
  • 函数的可选参数。

示例 1:使用可选参数

// 场景:生成欢迎语,称呼(title)是可选的
const greetUser = (name: string, title?: string): string => {
    if (title) {
        return `Hello, ${title} ${name}`;
    } else {
        return `Hello, ${name}`;
    }
};

// 只传必填参数
const user1: string = greetUser("Jack");
console.log(user1);

// 传满 2 个参数
const user2: string = greetUser("Jack", "Boss");
console.log(user2);

运行结果如下。

Hello, Jack
Hello, Boss Jack

分析:

在这个例子中,title 是一个可选参数。当我们只传入 "Jack" 时,TypeScript 不再报错,而是默默地将未传入的 title 赋值为 undefined。因此我们必须在函数体内使用 if (title) 来进行安全判断才行。

在使用可选参数时,有一条非常容易踩坑的铁律:可选参数必须放在所有必选参数的后面

如果我们把可选参数放在了必选参数的前面,那么 TypeScript 会立马报错,比如:

// 报错:必选参数不能位于可选参数后
const greetUserError = (title?: string, name: string) => { ... }

这其实很好理解,如果你前面没传值,后面的值传进来了,那么 TypeScript 编译器怎么知道你传的值到底是给前面那个参数的,还是给后面那个参数的呢?所以,可选参数必须统统往后放。

可选参数的底层逻辑

在前面我们学过,开启严格模式后,变量不能被随意赋值为空。那为什么这里的 lastName 没传值(变成了 undefined)却没有报错呢?

实际上,当我们在参数后面加上 “?” 后,TypeScript 编译器会在底层自动帮你做一次 “联合类型” 的转换。对于 title?: string 来说,TypeScript 眼中看到的真实类型其实是:

title: string | undefined

这也意味着,除了完全不传这个参数,我们甚至可以显式地传入一个 undefined,这在 TypeScript 看来也是绝对合法的:

// 显式传入 undefined,等同于没传
const user3: string = greetUser("Lucy", undefined);

TypeScript 函数的默认参数 (=)

除了可选参数,还有一种很常见的业务场景:如果用户没传这个参数,我们希望系统自动给它一个 “默认值”,而不是干巴巴的 undefined。

在 TypeScript 中(其实 ES6+ 中也是一样),我们可以直接使用等号 “=” 来给参数设置默认值。

语法:

// 传统函数声明
function 函数名(参数名: 类型 = 默认值): 返回值类型 {
    // 函数体逻辑
}

// 箭头函数声明
const 函数名 = (参数名: 类型 = 默认值): 返回值类型 => {
    // 函数体逻辑
};

示例 2:使用默认参数

// 场景:生成欢迎语,为 title 参数设置默认值为 "Guest"(访客)
const greetUser = (name: string, title: string = "Guest"): string => {
    return `Hello, ${title} ${name}`;
};

// 不传第 2 个参数,触发默认值
const finalMsg1 = greetUser("Jack");
console.log("最终欢迎语 A:", finalMsg1);

// 传入第 2 个参数,覆盖默认值
const finalMsg2 = greetUser("Jack", "Boss");
console.log("最终欢迎语 B:", finalMsg2);

运行结果如下。

最终欢迎语 A:Hello, Guest Jack
最终欢迎语 B:Hello, Boss Jack

分析:

当参数有了默认值之后,我们在调用时就可以选择性地忽略它。而且 TypeScript 非常聪明,如果我们给了默认值 "Guest",它就能通过 “类型推断” 知道 title 是个 string 类型,所以我们在参数里写成 title = "Guest",甚至都不用写 : string,代码变得更加清爽。

TypeScript 默认参数的 “奇葩” 站位

我们前面刚说了,可选参数(?)必须放到参数列表的最后面。那么,默认参数(=)也必须放在最后面吗?答案是:不需要。默认参数可以放在参数列表的任何位置。

但是,这就引出了一个非常经典的高级前端面试题:如果带默认值的参数放在了前面,我们在调用时,又该如何跳过它,去传后面的必填参数呢?

示例 3:默认参数在前面的 “越过” 技巧

// 场景:生成欢迎语,“默认参数 title” 放在 “必选参数 name” 的前面
const greetUser = (title: string = "Guest", name: string): string => {
    return `Hello, ${title} ${name}`;
};

// 必须显式传入 undefined 来触发 title 的默认值
const result2 = greetUser(undefined, "Jack");
console.log(result2);

运行结果如下。

Hello, Guest Jack

分析:

在这个例子中,我们在定义函数时,“默认参数 title” 放在了 “必选参数 name” 的前面。因此在调用函数时,第 1 个参数必须显式传入 undefined 来触发默认值。

如果我们直接传一个字符串(如下所示),此时该字符串会塞给 title,然后导致 name 缺失。

// 报错:应有 2 个参数,但获得 1 个
const result1 = greetUser("Jack");

在 TypeScript 的底层机制中,只有当我们确切地传入 undefined 时,编译器才会说:“哦,你不想给这个参数传值,那我就用它的默认值吧。” 这个小技巧在封装一些多参数的复杂底层工具库时非常有用。

最后,我们来总结一下这两种参数:

  • 可选参数(?):侧重于 “有没有”。如果没传,则底层就是 undefined。其中,可选参数必须放在参数列表的最后。
  • 默认参数(=):侧重于 “用什么”。如果没传,就用预设好的默认值。其中,默认参数可以放在任何位置。
给站长反馈

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

邮箱:lvyenet@vip.qq.com

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