作者选择了COVID-19救济基金,作为Write for DOnations计划的一部分接受捐赠。
简介
创建和使用函数是任何编程语言的一个基本方面,TypeScript也不例外。TypeScript完全支持现有的JavaScript语法的函数,同时还增加了类型信息和函数重载的新功能。除了为函数提供额外的文档,类型信息也减少了代码中出现错误的机会,因为将无效的数据类型传递给类型安全的函数的风险较低。
在本教程中,你将从创建带有类型信息的最基本的函数开始,然后进入更复杂的场景,如使用休息参数和函数重载。你将尝试不同的代码样本,你可以在你自己的TypeScript环境或TypeScript Playground(一个允许你直接在浏览器中编写TypeScript的在线环境)中跟随。
先决条件
要学习本教程,你需要。
- 一个环境,你可以在其中执行TypeScript程序,以跟随例子的发展。要在你的本地机器上设置这个,你需要以下东西。
- 同时安装Node和npm(或yarn),以便运行一个处理TypeScript相关包的开发环境。本教程在Node.js 14.3.0版本和npm 6.14.5版本中进行了测试。要在macOS或Ubuntu 18.04上安装,请按照《如何在macOS上安装Node.js并创建本地开发环境》中的步骤,或者按照《如何在Ubuntu 18.04上安装Node.js》中的《使用PPA安装》部分进行安装。如果你使用Windows Subsystem for Linux (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
复制代码
例如,在你的getUserFullName
函数中添加一个可选的prefix
参数,这是一个可选的字符串,可以作为用户的全名的前缀添加。
type User = {
firstName: string;
lastName: string;
};
function getUserFullName(user: User, prefix?: string) {
return `${prefix ?? ''}${user.firstName} ${user.lastName}`;
}
复制代码
在这个代码块的第一个高亮部分,你正在向你的函数添加一个可选的prefix
参数,而在第二个高亮部分,你正在用它给用户的全名加前缀。为了做到这一点,你使用了nullish凝聚运算符??
。这样,只有在定义了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
是承诺解析的值的类型。
想象一下,你有一个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
是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
,这正是你的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;
}
复制代码
为休息参数添加类型
休息参数是JavaScript中的一项功能,它允许一个函数作为一个单一的数组接收许多参数。在本节中,你将使用TypeScript的休息参数。
以类型安全的方式使用休息参数,完全可以通过使用休息参数后的结果数组的类型来实现。以下面的代码为例,你有一个名为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
布尔值添加到数组中。在使用该数组之前,你可以使用 .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的更多教程,请查看我们的《如何在TypeScript中编码》系列页面。