作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
创建和使用函数是任何编程语言的基本方面,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+ 语法,例如 解构、剩余运算符 和 导入/导出 。 如果您需要有关这些主题的更多信息,建议阅读我们的 如何在 JavaScript 中编码系列。
- 本教程将参考支持 TypeScript 并显示内联错误的文本编辑器的各个方面。 这不是使用 TypeScript 所必需的,但确实可以更多地利用 TypeScript 功能。 为了获得这些好处,您可以使用像 Visual Studio Code 这样的文本编辑器,它完全支持开箱即用的 TypeScript。 您还可以在 TypeScript Playground 中尝试这些好处。
本教程中显示的所有示例都是使用 TypeScript 4.2.2 版创建的。
创建类型化函数
在本节中,您将在 TypeScript 中创建函数,然后向它们添加类型信息。
在 JavaScript 中,可以通过多种方式声明函数。 最流行的一种是使用 function
关键字,如下所示:
function sum(a, b) { return a + b; }
在本例中,sum
是函数名,(a, b)
是参数,{return a + b;}
是函数体。
在 TypeScript 中创建函数的语法是相同的,除了一个主要的补充:您可以让编译器知道每个参数或参数应该具有什么类型。 以下代码块显示了一般语法,突出显示了类型声明:
function functionName(param1: Param1Type, param2: Param2Type): ReturnType { // ... body of the function }
使用此语法,您可以向前面显示的 sum
函数的参数添加类型:
function sum(a: number, b: number) { return a + b; }
这可确保 a
和 b
是 number
值。
您还可以添加返回值的类型:
function sum(a: number, b: number): number { return a + b; }
现在 TypeScript 将期望 sum
函数返回一个数值。 如果您使用一些参数调用函数并将结果值存储在名为 result
的变量中:
const result = sum(1, 2);
result
变量将具有 number
类型。 如果您正在使用 TypeScript 游乐场或使用完全支持 TypeScript 的文本编辑器,将光标悬停在 result
上将显示 const result: number
,表明 TypeScript 已从函数声明中隐含其类型。
如果你调用你的函数的值的类型不是你的函数预期的类型,TypeScript 编译器 (tsc
) 会给你错误 2345
。 对 sum
函数进行以下调用:
sum('shark', 'whale');
这将给出以下内容:
OutputArgument of type 'string' is not assignable to parameter of type 'number'. (2345)
您可以在函数中使用任何类型,而不仅仅是 基本类型 。 例如,假设您有一个如下所示的 User
类型:
type User = { firstName: string; lastName: string; };
您可以创建一个返回用户全名的函数,如下所示:
function getUserFullName(user: User): string { return `${user.firstName} ${user.lastName}`; }
大多数时候 TypeScript 足够聪明,可以推断出函数的返回类型,因此在这种情况下,您可以从函数声明中删除返回类型:
function getUserFullName(user: User) { return `${user.firstName} ${user.lastName}`; }
请注意,您删除了 : string
部分,这是您的函数的返回类型。 当您在函数体中返回字符串时,TypeScript 正确地假定您的函数具有字符串返回类型。
要现在调用您的函数,您必须传递一个与 User
类型具有相同形状的对象:
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
常量上,编辑器会将其类型识别为 string
。
TypeScript 中的可选函数参数
创建函数时并不总是需要所有参数。 在本节中,您将学习如何在 TypeScript 中将函数参数标记为可选。
要将函数参数转换为可选参数,请在参数名称后添加 ?
修饰符。 给定一个类型为 T
的函数参数 param1
,您可以通过添加 ?
使 param1
成为可选参数,如下所示:
param1?: T
例如,将可选的 prefix
参数添加到 getUserFullName
函数,这是一个可选字符串,可以作为前缀添加到用户的全名:
type User = { firstName: string; lastName: string; }; function getUserFullName(user: User, prefix?: string) { return `${prefix ?? ''}${user.firstName} ${user.lastName}`; }
在此代码块的第一个突出显示部分中,您正在向函数添加一个可选的 prefix
参数,在第二个突出显示部分中,您将在用户全名前面加上它。 为此,您正在使用 空值合并运算符 ??。 这样,如果已定义,您将只使用 prefix
值; 否则,该函数将使用空字符串。
现在您可以使用或不使用前缀参数调用您的函数,如下所示:
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 编译器返回错误 1016
:
OutputA required parameter cannot follow an optional parameter. (1016)
键入的箭头函数表达式
到目前为止,本教程已经展示了如何在 TypeScript 中键入普通函数,使用 function
关键字定义。 但是在 JavaScript 中,您可以通过多种方式定义函数,例如使用 箭头函数 。 在本节中,您将向 TypeScript 中的箭头函数添加类型。
向箭头函数添加类型的语法与向普通函数添加类型几乎相同。 为了说明这一点,将 getUserFullName
函数更改为箭头函数表达式:
const getUserFullName = (user: User, prefix?: string) => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
如果您想明确说明函数的返回类型,可以在 ()
之后添加它,如以下块中突出显示的代码所示:
const getUserFullName = (user: User, prefix?: string): string => `${prefix ?? ''}${user.firstName} ${user.lastName}`;
现在你可以像以前一样使用你的函数了:
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 中的函数有效。 有关这些规则的复习,请查看我们的 如何在 JavaScript 中定义函数教程 。
函数类型
在前面的部分中,您向 TypeScript 中的函数的参数和返回值添加了类型。 在本节中,您将学习如何创建函数类型,它们是表示特定函数签名的类型。 在将函数传递给其他函数时,创建与特定函数匹配的类型特别有用,例如具有本身就是函数的参数。 这是创建接受 回调 的函数时的常见模式。
创建函数类型的语法类似于创建箭头函数,但有两点不同:
- 您删除了函数体。
- 您使函数声明返回
return
类型本身。
以下是创建与您一直使用的 getUserFullName
函数匹配的类型的方法:
type User = { firstName: string; lastName: string; }; type PrintUserNameFunction = (user: User, prefix?: string) => string;
在此示例中,您使用 type
关键字声明了一个新类型,然后为括号中的两个参数提供了类型,并为箭头后面的返回值提供了类型。
对于更具体的示例,假设您正在创建一个名为 onEvent
的 事件侦听器函数 ,它接收事件名称作为第一个参数,第二个参数接收事件回调。 事件回调本身将接收具有以下类型的对象作为第一个参数:
type EventContext = { value: string; };
然后,您可以像这样编写 onEvent
函数:
type EventContext = { value: string; }; function onEvent(eventName: string, eventCallback: (target: EventContext) => void) { // ... implementation }
请注意,eventCallback
参数的类型是函数类型:
eventCallback: (target: EventTarget) => void
这意味着您的 onEvent
函数需要在 eventCallback
参数中传递另一个函数。 此函数应接受 EventTarget
类型的单个参数。 您的 onEvent
函数忽略了此函数的返回类型,因此您使用 void 作为类型。
使用类型化异步函数
在使用 JavaScript 时,使用 异步函数 是比较常见的。 TypeScript 有一种特定的方法来处理这个问题。 在本节中,您将在 TypeScript 中创建异步函数。
创建异步函数的语法与用于 JavaScript 的语法相同,但添加了允许类型:
async function asyncFunction(param1: number) { // ... function implementation ... }
向普通函数添加类型和向异步函数添加类型之间有一个主要区别:在异步函数中,返回类型必须始终是 Promise<T>
泛型。 Promise<T>
泛型表示由异步函数返回的 Promise 对象,其中 T
是 promise 解析为的值的类型。
假设你有一个 User
类型:
type User = { id: number; firstName: string; };
还想象一下,您在数据存储中有一些用户对象。 这些数据可以存储在任何地方,例如文件、数据库或 API 请求后面。 为简单起见,在此示例中,您将使用 array:
type User = { id: number; firstName: string; }; const users: User[] = [ { id: 1, firstName: "Jane" }, { id: 2, firstName: "Jon" } ];
如果您想创建一个类型安全的函数,以异步方式按 ID 检索用户,您可以这样做:
async function getUserById(userId: number): Promise<User | null> { const foundUser = users.find(user => user.id === userId); if (!foundUser) { return null; } return foundUser; }
在此函数中,您首先将函数声明为异步:
async function getUserById(userId: number): Promise<User | null> {
然后您指定它接受用户 ID 作为第一个参数,该用户 ID 必须是 number
:
async function getUserById(userId: number): Promise<User | null> {
getUserById
的返回类型是 Promise,可解析为 User
或 null
。 您正在使用 联合类型 User | null
作为 Promise
泛型的类型参数。
User | null
是 Promise<T>
中的 T
:
async function getUserById(userId: number): Promise<User | null> {
使用 await
调用您的函数并将结果存储在名为 user
的变量中:
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
:
Output'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module. (1375)
如果您在编辑器或 TypeScript Playground 中将鼠标悬停在 user
上,您会发现 user
的类型为 User | null
,这正是您的 promise 返回的类型getUserById
函数解析为。
如果删除 await
并直接调用该函数,则返回 Promise 对象:
async function runProgram() { const userPromise = getUserById(1); }
如果您将鼠标悬停在 userPromise
上,您会发现它的类型为 Promise<User | null>
。
大多数时候,TypeScript 可以推断异步函数的返回类型,就像它对非异步函数所做的那样。 因此,您可以省略 getUserById
函数的返回类型,因为它仍然可以正确推断为具有类型 Promise<User | null>
:
async function getUserById(userId: number) { const foundUser = users.find(user => user.id === userId); if (!foundUser) { return null; } return foundUser; }
为 Rest 参数添加类型
Rest parameters 是 JavaScript 中的一项功能,它允许函数以单个数组的形式接收许多参数。 在本节中,您将在 TypeScript 中使用剩余参数。
通过使用 rest 参数后跟结果数组的类型,完全可以以类型安全的方式使用 rest 参数。 以下面的代码为例,其中有一个名为 sum
的函数,它接受可变数量的数字并返回它们的总和:
function sum(...args: number[]) { return args.reduce((accumulator, currentValue) => { return accumulator + currentValue; }, 0); }
此函数使用 .reduce Array 方法 迭代数组并将元素相加。 请注意此处突出显示的其余参数 args
。 类型被设置为一个数字数组:number[]
。
调用您的函数正常工作:
function sum(...args: number[]) { return args.reduce((accumulator, currentValue) => { return accumulator + currentValue; }, 0); } const sumResult = sum(2, 4, 6, 8);
如果您使用数字以外的任何内容调用您的函数,例如:
const sumResult = sum(2, "b", 6, 8);
TypeScript 编译器将发出错误 2345
:
OutputArgument of type 'string' is not assignable to parameter of type 'number'. (2345)
使用函数重载
程序员有时需要一个函数来接受不同的参数,具体取决于函数的调用方式。 在 JavaScript 中,这通常是通过有一个参数来完成的,该参数可以采用不同类型的值,如字符串或数字。 将多个实现设置为相同的函数名称称为函数重载。
使用 TypeScript,您可以创建函数重载,明确描述它们处理的不同情况,通过分别记录重载函数的每个实现来改善开发人员体验。 本节将介绍如何在 TypeScript 中使用函数重载。
假设你有一个 User
类型:
type User = { id: number; email: string; fullName: string; age: number; };
并且您想创建一个可以使用以下任何信息查找用户的函数:
id
email
age
和fullName
您可以像这样创建这样的函数:
function getUser(idOrEmailOrAge: number | string, fullName?: string): User | undefined { // ... code }
此函数使用 |
运算符为 idOrEmailOrAge
和返回值组合类型的联合。
接下来,为您希望使用函数的每种方式添加函数重载,如以下突出显示的代码所示:
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
、email
或 age
:
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
函数的所有重载兼容。
您还为函数添加了一个可选参数,当用户传递 fullName
时:
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 }
实现您的函数可能如下所示,其中您使用 users
数组作为用户的数据存储:
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
是一个字符串,那么您可以使用 email
键搜索用户。 以下条件假设 idOrEmailOrAge
是一个数字,因此它是 id
或 age
,具体取决于是否定义了 fullName
。
函数重载的一个有趣方面是,在大多数编辑器中,包括 VS Code 和 TypeScript Playground,只要你键入函数名称并打开第一个括号来调用函数,就会出现一个弹出窗口,其中包含所有可用的重载,如下图所示:
如果您为每个函数重载添加注释,该注释也将作为文档来源出现在弹出窗口中。 例如,将以下突出显示的注释添加到示例重载中:
... /** * 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
函数返回过滤的数据数组时特别有用。
有条件地向数组添加值时的一项常见任务是检查某些条件,然后仅在条件为真时才添加值。 如果该值不为真,则代码将 false
Boolean 添加到数组中。 在使用该数组之前,您可以使用 .filter(Boolean)
对其进行过滤,以确保仅返回真值。
当使用值调用时,布尔构造函数返回 true
或 false
,具体取决于此值是 Truthy
还是 Falsy
值。
例如,假设您有一个字符串数组,并且如果其他标志为真,您只想将字符串 production
包含到该数组中:
const isProduction = false const valuesArray = ['some-string', isProduction && 'production'] function processArray(array: string[]) { // do something with array } processArray(valuesArray.filter(Boolean))
虽然这是在运行时完全有效的代码,但 TypeScript 编译器会在编译期间为您提供错误 2345
:
OutputArgument 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 | string
值的数组,这不是 processArray
所期望的。 它需要一个字符串数组:string[]
。
这是 TypeScript 不够聪明的一种情况,无法推断通过使用 .filter(Boolean)
您正在从数组中删除所有 falsy
值。 但是,有一种方法可以向 TypeScript 提供这个提示:使用用户定义的类型保护。
创建一个名为 isString
的用户定义类型保护函数:
function isString(value: any): value is string { return typeof value === "string" }
注意 isString
函数的返回类型。 创建用户定义类型保护的方法是使用以下语法作为函数的返回类型:
parameterName is Type
其中 parameterName
是您正在测试的参数的名称,而 Type
是此函数返回 true
时该参数值的预期类型。
在这种情况下,如果 isString
返回 true
,则 value
是 string
。 您还将 value
参数的类型设置为 any
,因此它适用于 any
类型的值。
现在,更改您的 .filter
调用以使用您的新函数,而不是将其传递给 Boolean
构造函数:
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 中构建类型安全的函数,以及如何利用函数重载来更好地记录单个函数的所有变体。 拥有这些知识将允许在整个代码中使用更多类型安全且易于维护的功能。
有关 TypeScript 的更多教程,请查看我们的 How To Code in TypeScript 系列页面。