英文 | https://www.digitalocean.com/community/tutorials/how-to-use-functions-in-typescript
翻译 | 杨小爱
介绍
创建和使用函数是任何编程语言的基本内容,TypeScript 也不例外。TypeScript 完全支持现有的 JavaScript 函数语法,同时,还添加了类型信息和函数重载作为新特性。除了为函数提供额外的文档外,类型信息还可以减少代码中出现错误的机会,因为将无效数据类型传递给类型安全函数的风险较低。
在本教程中,我们将从使用类型信息创建最基本的函数开始,然后,转到更复杂的场景,例如,使用剩余参数和函数重载。我们将尝试不同的代码示例,我们可以在自己的 TypeScript 环境或 TypeScript Playground(一个允许我们直接在浏览器中编写 TypeScript 的在线环境)中遵循这些示例。
准备工作
要完成本教程内容,我们需要做如下准备工作:
一个环境,我们可以在其中执行 TypeScript 程序以跟随示例。要在本地计算机上进行设置,我们将需要以下内容:
为了运行处理 TypeScript 相关包的开发环境,同时,安装了 Node 和 npm(或 yarn)。本教程使用 Node.js 版本 14.3.0 和 npm 版本 6.14.5 进行了测试。要在 macOS 或 Ubuntu 18.04 上安装,请按照如何在 macOS 上安装 Node.js 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。如果使用的是适用于 Linux 的 Windows 子系统 (WSL),这也适用。
此外,我们需要在机器上安装 TypeScript 编译器 (tsc)。为此,请参阅官方 TypeScript 网站。
如果不想在本地机器上创建 TypeScript 环境,可以使用官方的 TypeScript Playground 来跟随。
将需要足够的 JavaScript 知识,尤其是 ES6 语法,例如解构、rest 运算符和导入/导出。如果需要有关这些主题的更多知识,建议阅读我们的JavaScript 系列教程。
本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。为了获得这些好处,我们可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。我们也可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
创建类型化函数
在本节中,我们将在 TypeScript 中创建函数,然后向它们添加类型信息。
在 JavaScript 中,可以通过多种方式声明函数。最流行的一种是使用 function 关键字,如下所示:
代码语言:javascript复制function sum(a, b) {
return a b;
}
在本例中,sum 是函数的名称,(a, b) 是参数,{return a b;} 是函数体。
在 TypeScript 中创建函数的语法是相同的,除了一个主要的补充:我们可以让编译器知道每个参数或参数应该具有什么类型。以下代码块显示了一般语法,突出显示了类型声明:
代码语言:javascript复制function functionName(param1: Param1Type, param2: Param2Type): ReturnType {
// ... body of the function
}
使用此语法,我们可以将类型添加到前面显示的 sum 函数的参数:
代码语言:javascript复制function sum(a: number, b: number) {
return a b;
}
这确保 a 和 b 是数值。
我们还可以添加返回值的类型:
代码语言:javascript复制function sum(a: number, b: number): number {
return a b;
}
现在 TypeScript 将期望 sum 函数返回一个数字值。如果我们使用一些参数调用函数并将结果值存储在名为 result 的变量中:
代码语言:javascript复制const result = sum(1, 2);
结果变量将具有类型编号。如果我们正在使用 TypeScript 游乐场或使用完全支持 TypeScript 的文本编辑器,将光标悬停在 result 上将显示 const result: number,表明 TypeScript 从函数声明中隐含了它的类型。
如果我们调用函数的值的类型与函数预期的类型不同,TypeScript 编译器 (tsc) 会给我们错误 2345。对 sum 函数执行以下调用:
代码语言:javascript复制sum('shark', 'whale');
这将给出以下内容:
代码语言:javascript复制Output
Argument of type 'string' is not assignable to parameter of type 'number'. (2345)
我们可以在函数中使用任何类型,而不仅仅是基本类型。例如,假设我们有一个看起来像这样的 User 类型:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
我们可以创建一个返回用户全名的函数,如下所示:
代码语言:javascript复制function getUserFullName(user: User): string {
return `${user.firstName} ${user.lastName}`;
}
大多数时候 TypeScript 足够聪明,可以推断出函数的返回类型,因此,在这种情况下,我们可以从函数声明中删除返回类型:
代码语言:javascript复制function getUserFullName(user: User) {
return `${user.firstName} ${user.lastName}`;
}
请注意,我们删除了 : string 部分,它是函数的返回类型。当我们在函数体中返回字符串时,TypeScript 正确地假定我们的函数具有字符串返回类型。
要现在调用我们的函数,我们必须传递一个与 User 类型具有相同形状的对象:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User) {
return `${user.firstName} ${user.lastName}`;
}
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
此代码将成功通过 TypeScript 类型检查器。如果我们将鼠标悬停在编辑器中的 userFullName 常量上,编辑器会将其类型识别为字符串。
TypeScript 中的可选函数参数
创建函数时并不总是需要所有参数。在本节中,我们将学习如何在 TypeScript 中将函数参数标记为可选。
要将函数参数转换为可选参数,请添加 ? 参数名称后面的修饰符。给定一个类型为 T 的函数参数 param1,我们可以通过添加 ? 使 param1 成为可选参数,如下所示:
代码语言:javascript复制param1?: T
例如,为我们的 getUserFullName 函数添加一个可选的前缀参数,它是一个可选字符串,可以作为前缀添加到用户的全名:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User, prefix?: string) {
return `${prefix ?? ''}${user.firstName} ${user.lastName}`;
}
在此代码块的第一个突出显示部分中,我们正在向函数添加一个可选的前缀参数,在第二个突出显示部分中,我们将使用它作为用户全名的前缀。为此,我们正在使用无效合并运算符 ??。这样,我们将仅使用已定义的前缀值;否则,该函数将使用空字符串。
现在,我们可以使用或不使用前缀参数调用我们的函数,如下所示:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User, prefix?: string) {
return `${prefix ?? ''} ${user.firstName} ${user.lastName}`;
}
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
const mrUserFullName = getUserFullName(user, 'Mr. ');
在这种情况下,userFullName 的值为 Jon Doe,而 mrUserFullName 的值为 Mr. Jon Doe。
请注意,我们不能在必需参数之前添加可选参数;它必须在系列的最后列出,就像 (user: User, prefix?: string) 一样。首先,列出它会使 TypeScript Compiler 返回错误 1016:
代码语言:javascript复制Output
A required parameter cannot follow an optional parameter. (1016)
键入的箭头函数表达式
到目前为止,本教程已经展示了如何在 TypeScript 中键入使用 function 关键字定义的普通函数。但在 JavaScript 中,我们可以通过多种方式定义函数,例如使用箭头函数。在本节中,我们将向 TypeScript 中的箭头函数添加类型。
向箭头函数添加类型的语法与向普通函数添加类型几乎相同。为了说明这一点,请将 getUserFullName 函数更改为箭头函数表达式:
代码语言:javascript复制const getUserFullName = (user: User, prefix?: string) => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
如果我们想明确说明函数的返回类型,可以在 () 之后添加它,如以下代码块中突出显示的代码所示:
代码语言:javascript复制const getUserFullName = (user: User, prefix?: string): string => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
现在,我们可以像以前一样使用你的函数了:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
const getUserFullName = (user: User, prefix?: string) => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
const user: User = {
firstName: "Jon",
lastName: "Doe"
};
const userFullName = getUserFullName(user);
这将毫无错误地通过 TypeScript 类型检查器。
注意:请记住,对 JavaScript 中的函数有效的所有内容也对 TypeScript 中的函数有效。
函数类型
在前面的内容中,我们向 TypeScript 中的函数的参数和返回值添加了类型。在本节中,我们将学习如何创建函数类型,它们是表示特定函数签名的类型。在将函数传递给其他函数时,创建与特定函数匹配的类型特别有用,例如,具有本身就是函数的参数。这是创建接受回调的函数时的常见模式。
创建函数类型的语法类似于创建箭头函数,但有两点不同:
- 我们删除了函数体。
- 我们使函数声明返回返回类型本身。
以下是创建与我们一直使用的 getUserFullName 函数匹配的类型的方法:
代码语言:javascript复制type User = {
firstName: string;
lastName: string;
};
type PrintUserNameFunction = (user: User, prefix?: string) => string;
在此示例中,我们使用 type 关键字声明了一个新类型,然后,为括号中的两个参数提供了类型,并为箭头后面的返回值提供了类型。
举一个更具体的例子,假设我们正在创建一个名为 onEvent 的事件侦听器函数,它接收事件名称作为第一个参数,第二个参数接收事件回调。事件回调本身将接收具有以下类型的对象作为第一个参数:
代码语言:javascript复制type EventContext = {
value: string;
};
然后,我们可以像这样编写 onEvent 函数:
代码语言:javascript复制type EventContext = {
value: string;
};
function onEvent(eventName: string, eventCallback: (target: EventContext) => void) {
// ... implementation
}
注意 eventCallback 参数的类型是一个函数类型:
代码语言:javascript复制eventCallback: (target: EventTarget) => void
这意味着我们的 onEvent 函数需要在 eventCallback 参数中传递另一个函数。此函数应接受 EventTarget 类型的单个参数。我们的 onEvent 函数会忽略此函数的返回类型,因此,我们使用 void 作为类型。
使用类型化异步函数
在使用 JavaScript 时,使用异步函数是比较常见的。TypeScript 有一种特定的方法来处理这个问题。在本节中,我们将在 TypeScript 中创建异步函数。
创建异步函数的语法与用于 JavaScript 的语法相同,但添加了允许类型:
代码语言:javascript复制async function asyncFunction(param1: number) {
// ... function implementation ...
}
向普通函数添加类型和向异步函数添加类型之间有一个主要区别:在异步函数中,返回类型必须始终是 Promise<T> 泛型。Promise<T> 泛型表示由异步函数返回的 Promise 对象,其中 T 是 promise 解析为的值的类型。
假设我们有一个用户类型:
代码语言:javascript复制type User = {
id: number;
firstName: string;
};
还想象一下,我们在数据存储中有一些用户对象。这些数据可以存储在任何地方,例如文件、数据库或 API 请求后面。为简单起见,在此示例中,我们将使用数组:
代码语言:javascript复制type User = {
id: number;
firstName: string;
};
const users: User[] = [
{ id: 1, firstName: "Jane" },
{ id: 2, firstName: "Jon" }
];
如果我们想创建一个类型安全的函数,以异步方式按 ID 检索用户,我们可以这样做:
代码语言:javascript复制async function getUserById(userId: number): Promise<User | null> {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
在此函数中,我们首先将函数声明为异步:
代码语言:javascript复制async function getUserById(userId: number): Promise<User | null> {
然后,我们指定它接受作为第一个参数的用户 ID,它必须是一个数字:
代码语言:javascript复制async function getUserById(userId: number): Promise<User | null> {
getUserById 的返回类型是一个 Promise,它解析为 User 或 null。我们正在使用联合类型 User | null 作为 Promise 泛型的类型参数。
用户 | null 是 Promise<T> 中的 T:
代码语言:javascript复制async function getUserById(userId: number): Promise<User | null> {
使用 await 调用我们的函数并将结果存储在名为 user 的变量中:
代码语言:javascript复制type User = {
id: number;
firstName: string;
};
const users: User[] = [
{ id: 1, firstName: "Jane" },
{ id: 2, firstName: "Jon" }
];
async function getUserById(userId: number): Promise<User | null> {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
async function runProgram() {
const user = await getUserById(1);
}
注意:我们正在使用一个名为 runProgram 的包装函数,因为,我们不能在文件的顶层使用 await。这样做会导致 TypeScript 编译器发出错误 1375:
输出'await' 表达式仅在文件是模块时才允许在文件的顶层使用,但该文件没有导入或导出。考虑添加一个空的“export {}”以使该文件成为一个模块。(1375)
如果我们在编辑器或 TypeScript Playground 中将鼠标悬停在 user 上,我们会发现 user 的类型为 User | null,这正是我们的 getUserById 函数返回的承诺解析为的类型。
如果删除 await 并直接调用该函数,则返回 Promise 对象:
代码语言:javascript复制async function runProgram() {
const userPromise = getUserById(1);
}
如果,我们将鼠标悬停在 userPromise 上,我们会发现它的类型是 Promise<User | null>。
大多数时候,TypeScript 可以推断异步函数的返回类型,就像它对非异步函数所做的那样。
因此,您可以省略 getUserById 函数的返回类型,因为它仍然被正确推断为具有类型 Promise<User | null>:
代码语言:javascript复制async function getUserById(userId: number) {
const foundUser = users.find(user => user.id === userId);
if (!foundUser) {
return null;
}
return foundUser;
}
为 Rest 参数添加类型
剩余参数是 JavaScript 中的一项功能,它允许函数以单个数组的形式接收许多参数。在本节中,我们将在 TypeScript 中使用剩余参数。
通过使用 rest 参数后跟结果数组的类型,完全可以以类型安全的方式使用 rest 参数。以下面的代码为例,其中有一个名为 sum 的函数,它接受可变数量的数字并返回它们的总和:
代码语言:javascript复制function sum(...args: number[]) {
return args.reduce((accumulator, currentValue) => {
return accumulator currentValue;
}, 0);
}
该函数使用 .reduce Array 方法迭代数组并将元素相加。请注意此处突出显示的其余参数 args。类型被设置为一个数字数组:number[]。
调用我们的函数正常工作:
代码语言:javascript复制function sum(...args: number[]) {
return args.reduce((accumulator, currentValue) => {
return accumulator currentValue;
}, 0);
}
const sumResult = sum(2, 4, 6, 8);
如果我们使用数字以外的任何内容调用我们的函数,例如:
代码语言:javascript复制const sumResult = sum(2, "b", 6, 8);
TypeScript 编译器将发出错误 2345:
代码语言:javascript复制Output
Argument of type 'string' is not assignable to parameter of type 'number'. (2345)
使用函数重载
程序员有时需要一个函数来接受不同的参数,具体取决于函数的调用方式。在 JavaScript 中,这通常是通过有一个参数来完成的,该参数可以采用不同类型的值,如字符串或数字。将多个实现设置为相同的函数名称称为函数重载。
使用 TypeScript,我们可以创建函数重载,明确描述它们处理的不同情况,通过分别记录重载函数的每个实现来改善开发人员体验。
本节将介绍如何在 TypeScript 中使用函数重载。
假设我们有一个用户类型:
代码语言:javascript复制type User = {
id: number;
email: string;
fullName: string;
age: number;
};
并且我们想创建一个可以使用以下任何信息查找用户的函数:
- ID
- 电子邮件
- 年龄和全名
我们可以像这样创建这样的函数:
代码语言:javascript复制function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
该函数使用 | 运算符为 idOrEmailOrAge 和返回值组成类型的联合。
接下来,为我们希望使用函数的每种方式添加函数重载,如以下突出显示的代码所示:
代码语言:javascript复制type User = {
id: number;
email: string;
fullName: string;
age: number;
};
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
此函数具有三个重载,每个重载一个用于检索用户。创建函数重载时,在函数实现本身之前添加函数重载。函数重载没有主体;他们只有参数列表和返回类型。
接下来,实现函数本身,它应该有一个与所有函数重载兼容的参数列表。在前面的示例中,我们的第一个参数可以是数字或字符串,因为它可以是 id、电子邮件或年龄:
代码语言:javascript复制function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
因此,我们在函数实现中将 idOrEmailorAge 参数的类型设置为 number | string。这样,它就与 getUser 函数的所有重载兼容。
我们还为函数添加了一个可选参数,用于当用户传递全名时:
代码语言:javascript复制function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
// ... code
}
实现的功能可能如下所示,其中,我们使用用户数组作为用户的数据存储:
代码语言:javascript复制type User = {
id: number;
email: string;
fullName: string;
age: number;
};
const users: User[] = [
{ id: 1, email: "jane_doe@example.com", fullName: "Jane Doe" , age: 35 },
{ id: 2, email: "jon_do@example.com", fullName: "Jon Doe", age: 35 }
];
function getUser(id: number): User | undefined;
function getUser(email: string): User | undefined;
function getUser(age: number, fullName: string): User | undefined;
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined {
if (typeof idOrEmailOrAge === "string") {
return users.find(user => user.email === idOrEmailOrAge);
}
if (typeof fullName === "string") {
return users.find(user => user.age === idOrEmailOrAge && user.fullName === fullName);
} else {
return users.find(user => user.id === idOrEmailOrAge);
}
}
const userById = getUser(1);
const userByEmail = getUser("jane_doe@example.com");
const userByAgeAndFullName = getUser(35, "Jon Doe");
在此代码中,如果 idOrEmailOrAge 是一个字符串,那么,我们可以使用电子邮件键搜索用户。以下条件假设 idOrEmailOrAge 是一个数字,因此,它是 id 或年龄,具体取决于是否定义了 fullName。
函数重载的一个有趣的方面是,在大多数编辑器中,包括 VS Code 和 TypeScript Playground,只要我们键入函数名称并打开第一个括号来调用函数,就会出现一个弹出窗口,其中包含所有可用的重载, 如下图所示:
如果我们为每个函数重载添加注释,该注释也将作为文档来源出现在弹出窗口中。例如,将以下突出显示的注释添加到示例重载中:
代码语言:javascript复制...
/**
* Get a user by their ID.
*/
function getUser(id: number): User | undefined;
/**
* Get a user by their email.
*/
function getUser(email: string): User | undefined;
/**
* Get a user by their age and full name.
*/
function getUser(age: number, fullName: string): User | undefined;
...
现在,当我们将鼠标悬停在这些函数上时,将为每个重载显示注释,如下面的动画所示:
用户定义的类型保护
本教程将检查 TypeScript 中函数的最后一个特性是用户定义的类型保护,它们是允许 TypeScript 更好地推断某些值的类型的特殊函数。这些守卫在条件代码块中强制执行某些类型,其中值的类型可能会根据情况而有所不同。这些在使用 Array.prototype.filter 函数返回过滤的数据数组时特别有用。
有条件地向数组添加值时的一项常见任务是检查某些条件,然后,仅在条件为真时才添加值。如果该值不为真,则代码向数组添加一个假布尔值。在使用该数组之前,我们可以使用 .filter(Boolean) 对其进行过滤,以确保仅返回真实值。
当使用值调用时,布尔构造函数返回 true 或 false,具体取决于此值是 Truthy 还是 Falsy 值。
例如,假设我们有一个字符串数组,并且如果其他标志为真,我们只想将字符串产生式包含到该数组中:
代码语言:javascript复制const isProduction = false
const valuesArray = ['some-string', isProduction && 'production']
function processArray(array: string[]) {
// do something with array
}
processArray(valuesArray.filter(Boolean))
虽然,这是在运行时完全有效的代码,但 TypeScript 编译器会在编译期间为我们提供错误 2345:
代码语言:javascript复制Output
Argument of type '(string | boolean)[]' is not assignable to parameter of type 'string[]'.
Type 'string | boolean' is not assignable to type 'string'.
Type 'boolean' is not assignable to type 'string'. (2345)
此错误表示,在编译时,传递给 processArray 的值被解释为 false | 的数组。字符串值,这不是 processArray 所期望的。它需要一个字符串数组:string[]。
这是 TypeScript 不够聪明的一种情况,无法通过使用 .filter(Boolean) 来推断我们正在从数组中删除所有虚假值。但是,有一种方法可以向 TypeScript 提供这个提示:使用用户定义的类型保护。
创建一个名为 isString 的用户定义类型保护函数:
代码语言:javascript复制function isString(value: any): value is string {
return typeof value === "string"
}
注意 isString 函数的返回类型。创建用户定义类型保护的方法是使用以下语法作为函数的返回类型:
代码语言:javascript复制parameterName is Type
其中 parameterName 是我们正在测试的参数的名称,Type 是此函数返回 true 时此参数值的预期类型。
在这种情况下,如果 isString 返回 true,则表示 value 是一个字符串。我们还将 value 参数的类型设置为 any,因此,它适用于任何类型的值。
现在,更改 .filter 调用以使用的新函数,而不是将其传递给布尔构造函数:
代码语言:javascript复制const isProduction = false
const valuesArray = ['some-string', isProduction && 'production']
function processArray(array: string[]) {
// do something with array
}
function isString(value: any): value is string {
return typeof value === "string"
}
processArray(valuesArray.filter(isString))
现在 TypeScript 编译器正确地推断出传递给 processArray 的数组只包含字符串,并且,我们的代码可以正确编译。
结论
函数是 TypeScript 中应用程序的构建块,在本教程中,我们学习了如何在 TypeScript 中构建类型安全的函数,以及如何利用函数重载来更好地记录单个函数的所有变体。拥有这些知识将允许在整个代码中使用更多类型安全且易于维护的功能。