我们都知道,JavaScript 中的函数是非常自由的,甚至有点过于 “自由奔放” 了。
举个简单例子,由于没有类型的限制,假如我们定义了一个需要传入 2 个参数的函数,别人在调用时可以传 3 个参数,也可以一个参数也不传,甚至可以把原本期望的数字传成对象。JavaScript 函数这种过于自由,往往就是大型项目中 Bug 的万恶之源。
为了解决这个问题,TypeScript 为函数设置了极其严格的约束。不管是传统的函数声明,还是 ES6 的箭头函数,我们都可以对其进行严格的类型约束。
TypeScript 传统函数的声明
在 TypeScript 中,给传统函数添加类型约束是非常简单的:我们只需要给函数的 “参数” 和 “返回值” 分别加上类型注解即可。
语法:
function 函数名(参数1: 类型, 参数2: 类型): 返回值类型 {
// 函数体
}示例 1:定义传统函数
// 定义函数
function add(x: number, y: number): number {
return x + y;
}
// 调用函数
const result1: number = add(10, 20);
console.log(result1);运行结果如下。
30分析:
对于这个例子来说,如果我们使用下面的错误调用方式,则 TypeScript 会直接报错。
// 错误调用 1:参数类型错误
const result2 = add(10, "20");
// 错误调用 2:参数数量不匹配
const result3 = add(10); 函数声明有了类型约束之后,TypeScript 会对函数的调用进行严格的 “双向校验”:既校验传进来的参数个数和类型对不对,又校验函数最终 return 出去的结果类型对不对。这种安全感是原生 JavaScript 绝对给不了的。
在上面的例子中,我们显式地为函数添加了 “:number” 来约束返回值。但实际上,TypeScript 的编译器非常聪明:如果你的函数体内有明确的 return 语句,它会根据 return 后面的运算结果,自动推断出该函数的返回值类型。
// 这里没有写返回值类型 ": number"
function addFast(x: number, y: number) {
return x + y; // TS 知道两个数字相加必定是数字,因此自动推断返回值为 number
}
const result: number = addFast(10, 20); // 完全合法,不会报错提示: 在真实项目开发中,对于内部逻辑简单的函数,很多高级开发者喜欢省略返回值类型,把工作交给 TypeScript 去自动推断,从而让代码更清爽。但对于对外暴露的复杂核心 API,我们依然建议显式写出返回值类型,以防止内部逻辑修改而导致返回值类型意外突变。
TypeScript 箭头函数的声明
有过实际开发经验的小伙伴都知道,现在的项目(如 Vue 或 React)已经很少使用传统的 “function 关键字” 来声明函数了,绝大部分都是使用 “箭头函数” 的方式来声明函数。
在 TypeScript 中,给箭头函数加上类型,看起来语法会稍微长一点,但核心逻辑(参数类型 + 返回值类型)是完全不变的。
语法:
const 函数名 = (参数1: 类型, 参数2: 类型): 返回值类型 => {
// 函数体
};示例 2:定义箭头函数
const add = (x: number, y: number): number => {
return x + y;
};
const total: number = add(10, 20);
console.log(total);运行结果如下。
30分析:
初学的小伙伴在刚接触 TypeScript 箭头函数时,会被密密麻麻的冒号和箭头搞晕。其实我们只需要记住一句话即可:在括号 “()” 里面的是参数及其类型,而在括号 “()” 后面的是返回值类型。
TypeScript 高阶函数与自动类型推断
学到这里,小伙伴们可能会觉得:“如果项目中所有的回调函数、箭头函数都要像上面这样严谨地手写一遍类型,那岂不是要累死?”别慌嘛,其实 TypeScript 编译器远比你想象的要聪明。
在很多高阶函数(如数组的 map()、filter()、forEach() 等)中,TypeScript 拥有一种极其强大的特性——上下文类型推断(Contextual Typing)。
示例 3:自动类型推断
const userNames: string[] = ["Jack", "Lucy", "Tony"];
// 无需手动为 name 声明类型
userNames.forEach((name) => {
console.log(name.toUpperCase());
});运行结果如下。
JACK
LUCY
TONY分析:
在这个例子中,我们并没有给匿名箭头函数里的参数 name 添加任何的 “: string” 约束。这是因为 TypeScript 会根据前面的数组 userNames,自动推断出了回调函数里参数的类型。
这就引出了我们在企业级开发中的一个最佳实践:虽然 TypeScript 允许我们给一切函数加上极其严格的类型,但如果是上下文能够清晰推断出的回调函数,我们应该大胆地省略类型声明!这不仅能让代码保持像原生 JavaScript 一样的清爽,还能白嫖 TypeScript 的类型安全校验。
TypeScript 把函数类型抽离出来
在真实的项目开发中,我们经常会遇到好几个函数长着同样的 “参数和返回值结构”。为了复用这个结构,我们可以结合上一章学过 “接口(interface)” 和 “类型别名(type)”,单独把 “函数长什么样” 给抽离出来。
示例 4:使用 type 抽离函数签名
// 定义类型别名
type CalculateFunc = (x: number, y: number) => number;
// 使用类型别名
const add: CalculateFunc = (x, y) => {
return x + y;
};
const result = add(10, 20);
console.log(result);运行结果如下。
30分析:
在这个例子中,我们定义一个专属的 “计算函数” 类型别名(即 CalculateFunc),然后就可以直接使用这个类型别名来约束箭头函数。这样,我们不仅不需要在函数体里写参数类型,甚至连返回值类型都可以省略了。
当我们将 CalculateFunc 赋予变量 add 后,TypeScript 就会自动将类型规则映射到后面的匿名箭头函数上。这种写法将 “类型声明” 和 “业务逻辑” 完美解耦,是很多开源框架中最常见的操作。
示例 5:使用 interface 抽离函数签名
// 使用接口定义函数长什么样
interface CalculateFunc {
(x: number, y: number): number;
}
// 使用接口约束箭头函数
const add: CalculateFunc = (x, y) => {
return x + y;
};
const result = add(10, 20);
console.log(result);运行结果如下。
30分析:
除了 type 之外,我们在上一章学过的 interface 其实也是可以用来约束函数的。这里很多小伙伴会问:“定义函数签名到底应该用 type,还是用 interface 呢?”
在绝大多数的企业级实战中,我们强烈推荐优先使用 type(也就是示例 4 的写法),因为它的语法非常直观,和正常的箭头函数长得一模一样。而 interface 的写法往往让人觉得有些别扭,通常只在构建复杂的第三方库声明文件时才会用到。
