作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。
介绍
创建和使用函数是任何编程语言的基本方面,TypeScript也不例外。TypeScript 完全支持函数的现有JavaScript 语法,同时还添加了类型信息和函数重载作为新功能。除了为函数提供额外的文档外,类型信息还减少了代码中出现错误的机会,因为将无效数据类型传递给类型安全函数的风险较低。
在本教程中,您将首先使用类型信息创建最基本的函数,然后转向更复杂的场景,例如使用其余参数和函数重载。您将尝试不同的代码示例,您可以在自己的 TypeScript 环境或TypeScript Playground 中进行操作,TypeScript Playground是一个允许您直接在浏览器中编写 TypeScript 的在线环境。
先决条件
要学习本教程,您需要:
- 您可以在其中执行 TypeScript 程序以跟随示例进行操作的环境。要在本地计算机上进行设置,您需要以下内容:
- 这两个节点和NPM(或丝安装才能运行的开发环境,手柄的打字稿相关的软件包)。本教程使用 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),这也适用。
- 此外,您将需要
tsc
在您的机器上安装TypeScript 编译器 ( )。为此,请参阅TypeScript 官方网站。
- 如果您不想在本地机器上创建 TypeScript 环境,您可以使用官方的TypeScript Playground进行操作。
- 您将需要足够的 JavaScript 知识,尤其是 ES6+ 语法,例如解构、rest 运算符和import/exports。如果您需要有关这些主题的更多信息,建议阅读我们的如何在 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 Compiler ( 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 中将函数参数标记为可选。
要将函数参数转换为可选参数,请?
在参数名称之后添加修饰符。给定一个param1
type的函数参数T
,您可以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
will的值是Jon Doe
,而mrUserFullName
will的值是Mr. Jon Doe
。
请注意,您不能在必需参数之前添加可选参数;它必须列在系列的最后,就像(user: User, prefix?: string)
. 首先列出它会使 TypeScript 编译器返回错误1016
:
OutputA required parameter cannot follow an optional parameter. (1016)
类型化箭头函数表达式
到目前为止,本教程已经展示了如何在使用function
关键字定义的 TypeScript 中键入普通函数。但是在 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>
通用表示通过异步函数,其中返回的无极对象T
是值的承诺解析为的类型。
想象一下你有一个User
类型:
type User = {
id: number;
firstName: string;
};
还想象一下,您在数据存储中有几个用户对象。这些数据可以存储在任何地方,比如文件、数据库或 API 请求后面。为简单起见,在此示例中,您将使用数组:
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 作为第一个参数,它必须是number
:
async function getUserById(userId: number): Promise<User | null> {
的返回类型getUserById
是解析为或的Promise。您正在使用联合类型作为泛型的类型参数。User
null
User | null
Promise
User | null
是T
在Promise<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)
如果您将鼠标悬停user
在编辑器或 TypeScript Playground 中,您会发现它user
具有 type User | null
,这正是您的getUserById
函数返回的Promise 解析为的类型。
如果去掉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 参数
其余参数是 JavaScript 中的一项功能,它允许函数将多个参数作为单个数组接收。在本节中,您将在 TypeScript 中使用 rest 参数。
通过使用 rest 参数后跟结果数组的类型,完全可以以类型安全的方式使用 rest 参数。以下面的代码为例,其中有一个函数被调用sum
,它接受可变数量的数字并返回它们的总和:
function sum(...args: number[]) {
return args.reduce((accumulator, currentValue) => {
return accumulator + currentValue;
}, 0);
}
此函数使用.reduce
Array 方法迭代数组并将元素相加。注意args
这里突出显示的 rest 参数。类型被设置为一个数字数组: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: "[email protected]", fullName: "Jane Doe" , age: 35 },
{ id: 2, email: "[email protected]", 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("[email protected]");
const userByAgeAndFullName = getUser(35, "Jon Doe");
在这段代码中,如果idOrEmailOrAge
是一个字符串,那么你可以用email
key搜索用户。以下条件假定idOrEmailOrAge
是一个数字,因此它是 theid
或 the age
,具体取决于 iffullName
的定义。
函数重载的一个有趣方面是,在大多数编辑器中,包括 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
布尔值。在使用该数组之前,您可以过滤它以确保只返回真值。.filter(Boolean)
当使用一个值调用时,布尔构造函数返回true
or false
,具体取决于该值是否为 aTruthy
或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
。
在这种情况下,您说的value
是string
ifisString
返回true
。您还将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 的更多教程,请查看我们的如何在 TypeScript 中编码系列页面。