介绍 创建和使用函数是任何编程语言的基本内容,TypeScript也不例外。TypeScript完全支持现有的JavaScript函数语法,同时,还添加了类型信息和函数重载作为新特性。除了为函数提供额外的文档外,类型信息还可以减少代码中出现错误的机会,因为将无效数据类型传递给类型安全函数的风险较低。 在本教程中,我们将从使用类型信息创建最基本的函数开始,然后,转到更复杂的场景,例如,使用剩余参数和函数重载。我们将尝试不同的代码示例,我们可以在自己的TypeScript环境或TypeScriptPlayground(一个允许我们直接在浏览器中编写TypeScript的在线环境)中遵循这些示例。 准备工作 要完成本教程内容,我们需要做如下准备工作: 一个环境,我们可以在其中执行TypeScript程序以跟随示例。要在本地计算机上进行设置,我们将需要以下内容: 为了运行处理TypeScript相关包的开发环境,同时,安装了Node和npm(或yarn)。本教程使用Node。js版本14。3。0和npm版本6。14。5进行了测试。要在macOS或Ubuntu18。04上安装,请按照如何在macOS上安装Node。js和创建本地开发环境或如何在Ubuntu18。04上安装Node。js的使用PPA安装部分中的步骤进行操作。如果使用的是适用于Linux的Windows子系统(WSL),这也适用。 此外,我们需要在机器上安装TypeScript编译器(tsc)。为此,请参阅官方TypeScript网站。 如果不想在本地机器上创建TypeScript环境,可以使用官方的TypeScriptPlayground来跟随。 将需要足够的JavaScript知识,尤其是ES6语法,例如解构、rest运算符和导入导出。如果需要有关这些主题的更多知识,建议阅读我们的JavaScript系列教程。 本教程将参考支持TypeScript并显示内联错误的文本编辑器的各个方面。这不是使用TypeScript所必需的,但确实可以更多地利用TypeScript功能。为了获得这些好处,我们可以使用像VisualStudioCode这样的文本编辑器,它完全支持开箱即用的TypeScript。我们也可以在TypeScriptPlayground中尝试这些好处。 本教程中显示的所有示例都是使用TypeScript4。2。2版创建的。 创建类型化函数 在本节中,我们将在TypeScript中创建函数,然后向它们添加类型信息。 在JavaScript中,可以通过多种方式声明函数。最流行的一种是使用function关键字,如下所示:functionsum(a,b){returnab;} 在本例中,sum是函数的名称,(a,b)是参数,{returnab;}是函数体。 在TypeScript中创建函数的语法是相同的,除了一个主要的补充:我们可以让编译器知道每个参数或参数应该具有什么类型。以下代码块显示了一般语法,突出显示了类型声明:functionfunctionName(param1:Param1Type,param2:Param2Type):ReturnType{。。。bodyofthefunction} 使用此语法,我们可以将类型添加到前面显示的sum函数的参数:functionsum(a:number,b:number){returnab;} 这确保a和b是数值。 我们还可以添加返回值的类型:functionsum(a:number,b:number):number{returnab;} 现在TypeScript将期望sum函数返回一个数字值。如果我们使用一些参数调用函数并将结果值存储在名为result的变量中:constresultsum(1,2); 结果变量将具有类型编号。如果我们正在使用TypeScript游乐场或使用完全支持TypeScript的文本编辑器,将光标悬停在result上将显示constresult:number,表明TypeScript从函数声明中隐含了它的类型。 如果我们调用函数的值的类型与函数预期的类型不同,TypeScript编译器(tsc)会给我们错误2345。对sum函数执行以下调用:sum(shark,whale); 这将给出以下内容:OutputArgumentoftypestringisnotassignabletoparameteroftypenumber。(2345) 我们可以在函数中使用任何类型,而不仅仅是基本类型。例如,假设我们有一个看起来像这样的User类型:typeUser{firstName:string;lastName:string;}; 我们可以创建一个返回用户全名的函数,如下所示:functiongetUserFullName(user:User):string{return{user。firstName}{user。lastName};} 大多数时候TypeScript足够聪明,可以推断出函数的返回类型,因此,在这种情况下,我们可以从函数声明中删除返回类型:functiongetUserFullName(user:User){return{user。firstName}{user。lastName};} 请注意,我们删除了:string部分,它是函数的返回类型。当我们在函数体中返回字符串时,TypeScript正确地假定我们的函数具有字符串返回类型。 要现在调用我们的函数,我们必须传递一个与User类型具有相同形状的对象:typeUser{firstName:string;lastName:string;};functiongetUserFullName(user:User){return{user。firstName}{user。lastName};}constuser:User{firstName:Jon,lastName:Doe};constuserFullNamegetUserFullName(user); 此代码将成功通过TypeScript类型检查器。如果我们将鼠标悬停在编辑器中的userFullName常量上,编辑器会将其类型识别为字符串。 TypeScript中的可选函数参数 创建函数时并不总是需要所有参数。在本节中,我们将学习如何在TypeScript中将函数参数标记为可选。 要将函数参数转换为可选参数,请添加?参数名称后面的修饰符。给定一个类型为T的函数参数param1,我们可以通过添加?使param1成为可选参数,如下所示:param1?:T 例如,为我们的getUserFullName函数添加一个可选的前缀参数,它是一个可选字符串,可以作为前缀添加到用户的全名:typeUser{firstName:string;lastName:string;};functiongetUserFullName(user:User,prefix?:string){return{prefix??}{user。firstName}{user。lastName};} 在此代码块的第一个突出显示部分中,我们正在向函数添加一个可选的前缀参数,在第二个突出显示部分中,我们将使用它作为用户全名的前缀。为此,我们正在使用无效合并运算符??。这样,我们将仅使用已定义的前缀值;否则,该函数将使用空字符串。 现在,我们可以使用或不使用前缀参数调用我们的函数,如下所示:typeUser{firstName:string;lastName:string;};functiongetUserFullName(user:User,prefix?:string){return{prefix??}{user。firstName}{user。lastName};}constuser:User{firstName:Jon,lastName:Doe};constuserFullNamegetUserFullName(user);constmrUserFullNamegetUserFullName(user,Mr。); 在这种情况下,userFullName的值为JonDoe,而mrUserFullName的值为Mr。JonDoe。 请注意,我们不能在必需参数之前添加可选参数;它必须在系列的最后列出,就像(user:User,prefix?:string)一样。首先,列出它会使TypeScriptCompiler返回错误1016:OutputArequiredparametercannotfollowanoptionalparameter。(1016) 键入的箭头函数表达式 到目前为止,本教程已经展示了如何在TypeScript中键入使用function关键字定义的普通函数。但在JavaScript中,我们可以通过多种方式定义函数,例如使用箭头函数。在本节中,我们将向TypeScript中的箭头函数添加类型。 向箭头函数添加类型的语法与向普通函数添加类型几乎相同。为了说明这一点,请将getUserFullName函数更改为箭头函数表达式:constgetUserFullName(user:User,prefix?:string){prefix??}{user。firstName}{user。lastName}; 如果我们想明确说明函数的返回类型,可以在()之后添加它,如以下代码块中突出显示的代码所示:constgetUserFullName(user:User,prefix?:string):string{prefix??}{user。firstName}{user。lastName}; 现在,我们可以像以前一样使用你的函数了:typeUser{firstName:string;lastName:string;};constgetUserFullName(user:User,prefix?:string){prefix??}{user。firstName}{user。lastName};constuser:User{firstName:Jon,lastName:Doe};constuserFullNamegetUserFullName(user); 这将毫无错误地通过TypeScript类型检查器。 注意:请记住,对JavaScript中的函数有效的所有内容也对TypeScript中的函数有效。 函数类型 在前面的内容中,我们向TypeScript中的函数的参数和返回值添加了类型。在本节中,我们将学习如何创建函数类型,它们是表示特定函数签名的类型。在将函数传递给其他函数时,创建与特定函数匹配的类型特别有用,例如,具有本身就是函数的参数。这是创建接受回调的函数时的常见模式。 创建函数类型的语法类似于创建箭头函数,但有两点不同:我们删除了函数体。我们使函数声明返回返回类型本身。 以下是创建与我们一直使用的getUserFullName函数匹配的类型的方法:typeUser{firstName:string;lastName:string;};typePrintUserNameFunction(user:User,prefix?:string)string; 在此示例中,我们使用type关键字声明了一个新类型,然后,为括号中的两个参数提供了类型,并为箭头后面的返回值提供了类型。 举一个更具体的例子,假设我们正在创建一个名为onEvent的事件侦听器函数,它接收事件名称作为第一个参数,第二个参数接收事件回调。事件回调本身将接收具有以下类型的对象作为第一个参数:typeEventContext{value:string;}; 然后,我们可以像这样编写onEvent函数:typeEventContext{value:string;};functiononEvent(eventName:string,eventCallback:(target:EventContext)void){。。。implementation} 注意eventCallback参数的类型是一个函数类型:eventCallback:(target:EventTarget)void 这意味着我们的onEvent函数需要在eventCallback参数中传递另一个函数。此函数应接受EventTarget类型的单个参数。我们的onEvent函数会忽略此函数的返回类型,因此,我们使用void作为类型。 使用类型化异步函数 在使用JavaScript时,使用异步函数是比较常见的。TypeScript有一种特定的方法来处理这个问题。在本节中,我们将在TypeScript中创建异步函数。 创建异步函数的语法与用于JavaScript的语法相同,但添加了允许类型:asyncfunctionasyncFunction(param1:number){。。。functionimplementation。。。} 向普通函数添加类型和向异步函数添加类型之间有一个主要区别:在异步函数中,返回类型必须始终是Promise泛型。Promise泛型表示由异步函数返回的Promise对象,其中T是promise解析为的值的类型。 假设我们有一个用户类型:typeUser{id:number;firstName:string;}; 还想象一下,我们在数据存储中有一些用户对象。这些数据可以存储在任何地方,例如文件、数据库或API请求后面。为简单起见,在此示例中,我们将使用数组:typeUser{id:number;firstName:string;};constusers:User〔〕〔{id:1,firstName:Jane},{id:2,firstName:Jon}〕; 如果我们想创建一个类型安全的函数,以异步方式按ID检索用户,我们可以这样做:asyncfunctiongetUserById(userId:number):PromiseUsernull{constfoundUserusers。find(useruser。iduserId);if(!foundUser){returnnull;}returnfoundUser;} 在此函数中,我们首先将函数声明为异步:asyncfunctiongetUserById(userId:number):PromiseUsernull{ 然后,我们指定它接受作为第一个参数的用户ID,它必须是一个数字:asyncfunctiongetUserById(userId:number):PromiseUsernull{ getUserById的返回类型是一个Promise,它解析为User或null。我们正在使用联合类型Usernull作为Promise泛型的类型参数。 用户null是Promise中的T:asyncfunctiongetUserById(userId:number):PromiseUsernull{ 使用await调用我们的函数并将结果存储在名为user的变量中:typeUser{id:number;firstName:string;};constusers:User〔〕〔{id:1,firstName:Jane},{id:2,firstName:Jon}〕;asyncfunctiongetUserById(userId:number):PromiseUsernull{constfoundUserusers。find(useruser。iduserId);if(!foundUser){returnnull;}returnfoundUser;}asyncfunctionrunProgram(){constuserawaitgetUserById(1);} 注意:我们正在使用一个名为runProgram的包装函数,因为,我们不能在文件的顶层使用await。这样做会导致TypeScript编译器发出错误1375: 输出await表达式仅在文件是模块时才允许在文件的顶层使用,但该文件没有导入或导出。考虑添加一个空的export{}以使该文件成为一个模块。(1375) 如果我们在编辑器或TypeScriptPlayground中将鼠标悬停在user上,我们会发现user的类型为Usernull,这正是我们的getUserById函数返回的承诺解析为的类型。 如果删除await并直接调用该函数,则返回Promise对象:asyncfunctionrunProgram(){constuserPromisegetUserById(1);} 如果,我们将鼠标悬停在userPromise上,我们会发现它的类型是Promise。 大多数时候,TypeScript可以推断异步函数的返回类型,就像它对非异步函数所做的那样。 因此,您可以省略getUserById函数的返回类型,因为它仍然被正确推断为具有类型Promise:asyncfunctiongetUserById(userId:number){constfoundUserusers。find(useruser。iduserId);if(!foundUser){returnnull;}returnfoundUser;} 为Rest参数添加类型 剩余参数是JavaScript中的一项功能,它允许函数以单个数组的形式接收许多参数。在本节中,我们将在TypeScript中使用剩余参数。 通过使用rest参数后跟结果数组的类型,完全可以以类型安全的方式使用rest参数。以下面的代码为例,其中有一个名为sum的函数,它接受可变数量的数字并返回它们的总和:functionsum(。。。args:number〔〕){returnargs。reduce((accumulator,currentValue){returnaccumulatorcurrentValue;},0);} 该函数使用。reduceArray方法迭代数组并将元素相加。请注意此处突出显示的其余参数args。类型被设置为一个数字数组:number〔〕。 调用我们的函数正常工作:functionsum(。。。args:number〔〕){returnargs。reduce((accumulator,currentValue){returnaccumulatorcurrentValue;},0);}constsumResultsum(2,4,6,8); 如果我们使用数字以外的任何内容调用我们的函数,例如:constsumResultsum(2,b,6,8); TypeScript编译器将发出错误2345:OutputArgumentoftypestringisnotassignabletoparameteroftypenumber。(2345) 使用函数重载 程序员有时需要一个函数来接受不同的参数,具体取决于函数的调用方式。在JavaScript中,这通常是通过有一个参数来完成的,该参数可以采用不同类型的值,如字符串或数字。将多个实现设置为相同的函数名称称为函数重载。 使用TypeScript,我们可以创建函数重载,明确描述它们处理的不同情况,通过分别记录重载函数的每个实现来改善开发人员体验。 本节将介绍如何在TypeScript中使用函数重载。 假设我们有一个用户类型:typeUser{id:number;email:string;fullName:string;age:number;}; 并且我们想创建一个可以使用以下任何信息查找用户的函数:ID电子邮件年龄和全名 我们可以像这样创建这样的函数:functiongetUser(idOrEmailOrAge:numberstring,fullName?:string):Userundefined{。。。code} 该函数使用运算符为idOrEmailOrAge和返回值组成类型的联合。 接下来,为我们希望使用函数的每种方式添加函数重载,如以下突出显示的代码所示:typeUser{id:number;email:string;fullName:string;age:number;};functiongetUser(id:number):Userundefined;functiongetUser(email:string):Userundefined;functiongetUser(age:number,fullName:string):Userundefined;functiongetUser(idOrEmailOrAge:numberstring,fullName?:string):Userundefined{。。。code} 此函数具有三个重载,每个重载一个用于检索用户。创建函数重载时,在函数实现本身之前添加函数重载。函数重载没有主体;他们只有参数列表和返回类型。 接下来,实现函数本身,它应该有一个与所有函数重载兼容的参数列表。在前面的示例中,我们的第一个参数可以是数字或字符串,因为它可以是id、电子邮件或年龄:functiongetUser(id:number):Userundefined;functiongetUser(email:string):Userundefined;functiongetUser(age:number,fullName:string):Userundefined;functiongetUser(idOrEmailOrAge:numberstring,fullName?:string):Userundefined{。。。code} 因此,我们在函数实现中将idOrEmailorAge参数的类型设置为numberstring。这样,它就与getUser函数的所有重载兼容。 我们还为函数添加了一个可选参数,用于当用户传递全名时:functiongetUser(id:number):Userundefined;functiongetUser(email:string):Userundefined;functiongetUser(age:number,fullName:string):Userundefined;functiongetUser(idOrEmailOrAge:numberstring,fullName?:string):Userundefined{。。。code} 实现的功能可能如下所示,其中,我们使用用户数组作为用户的数据存储:typeUser{id:number;email:string;fullName:string;age:number;};constusers:User〔〕〔{id:1,email:janedoeexample。com,fullName:JaneDoe,age:35},{id:2,email:jondoexample。com,fullName:JonDoe,age:35}〕;functiongetUser(id:number):Userundefined;functiongetUser(email:string):Userundefined;functiongetUser(age:number,fullName:string):Userundefined;functiongetUser(idOrEmailOrAge:numberstring,fullName?:string):Userundefined{if(typeofidOrEmailOrAgestring){returnusers。find(useruser。emailidOrEmailOrAge);}if(typeoffullNamestring){returnusers。find(useruser。ageidOrEmailOrAgeuser。fullNamefullName);}else{returnusers。find(useruser。ididOrEmailOrAge);}}constuserByIdgetUser(1);constuserByEmailgetUser(janedoeexample。com);constuserByAgeAndFullNamegetUser(35,JonDoe); 在此代码中,如果idOrEmailOrAge是一个字符串,那么,我们可以使用电子邮件键搜索用户。以下条件假设idOrEmailOrAge是一个数字,因此,它是id或年龄,具体取决于是否定义了fullName。 函数重载的一个有趣的方面是,在大多数编辑器中,包括VSCode和TypeScriptPlayground,只要我们键入函数名称并打开第一个括号来调用函数,就会出现一个弹出窗口,其中包含所有可用的重载,如下图所示: 如果我们为每个函数重载添加注释,该注释也将作为文档来源出现在弹出窗口中。例如,将以下突出显示的注释添加到示例重载中:。。。GetauserbytheirID。functiongetUser(id:number):Userundefined;Getauserbytheiremail。functiongetUser(email:string):Userundefined;Getauserbytheirageandfullname。functiongetUser(age:number,fullName:string):Userundefined;。。。 现在,当我们将鼠标悬停在这些函数上时,将为每个重载显示注释,如下面的动画所示: 用户定义的类型保护 本教程将检查TypeScript中函数的最后一个特性是用户定义的类型保护,它们是允许TypeScript更好地推断某些值的类型的特殊函数。这些守卫在条件代码块中强制执行某些类型,其中值的类型可能会根据情况而有所不同。这些在使用Array。prototype。filter函数返回过滤的数据数组时特别有用。 有条件地向数组添加值时的一项常见任务是检查某些条件,然后,仅在条件为真时才添加值。如果该值不为真,则代码向数组添加一个假布尔值。在使用该数组之前,我们可以使用。filter(Boolean)对其进行过滤,以确保仅返回真实值。 当使用值调用时,布尔构造函数返回true或false,具体取决于此值是Truthy还是Falsy值。 例如,假设我们有一个字符串数组,并且如果其他标志为真,我们只想将字符串产生式包含到该数组中:constisProductionfalseconstvaluesArray〔somestring,isProductionproduction〕functionprocessArray(array:string〔〕){dosomethingwitharray}processArray(valuesArray。filter(Boolean)) 虽然,这是在运行时完全有效的代码,但TypeScript编译器会在编译期间为我们提供错误2345:OutputArgumentoftype(stringboolean)〔〕isnotassignabletoparameteroftypestring〔〕。Typestringbooleanisnotassignabletotypestring。Typebooleanisnotassignabletotypestring。(2345) 此错误表示,在编译时,传递给processArray的值被解释为false的数组。字符串值,这不是processArray所期望的。它需要一个字符串数组:string〔〕。 这是TypeScript不够聪明的一种情况,无法通过使用。filter(Boolean)来推断我们正在从数组中删除所有虚假值。但是,有一种方法可以向TypeScript提供这个提示:使用用户定义的类型保护。 创建一个名为isString的用户定义类型保护函数:functionisString(value:any):valueisstring{returntypeofvaluestring} 注意isString函数的返回类型。创建用户定义类型保护的方法是使用以下语法作为函数的返回类型:parameterNameisType 其中parameterName是我们正在测试的参数的名称,Type是此函数返回true时此参数值的预期类型。 在这种情况下,如果isString返回true,则表示value是一个字符串。我们还将value参数的类型设置为any,因此,它适用于任何类型的值。 现在,更改。filter调用以使用的新函数,而不是将其传递给布尔构造函数:constisProductionfalseconstvaluesArray〔somestring,isProductionproduction〕functionprocessArray(array:string〔〕){dosomethingwitharray}functionisString(value:any):valueisstring{returntypeofvaluestring}processArray(valuesArray。filter(isString)) 现在TypeScript编译器正确地推断出传递给processArray的数组只包含字符串,并且,我们的代码可以正确编译。 结论 函数是TypeScript中应用程序的构建块,在本教程中,我们学习了如何在TypeScript中构建类型安全的函数,以及如何利用函数重载来更好地记录单个函数的所有变体。拥有这些知识将允许在整个代码中使用更多类型安全且易于维护的功能。