简介TypeScript是JavaScript语言的扩展,它使用JavaScript运行时和编译时类型检查器。TypeScript提供了几种在代码中表示对象的方法,其中一种是使用接口。TypeScript中的接口有两种使用场景:您可以创建类必须遵循的契约,例如,那些类必须实现的成员,您可以在应用程序中表示类型,就像普通的类型声明一样。您可能会注意到接口和类型共享一组相似的功能。事实上,一个几乎总是可以替代另一个。主要区别是接口可能有同一个接口的多个声明,TypeScript会合并这些声明,而类型只能声明一次。您还可以使用类型为基本类型(例如字符串和布尔值)创建别名,这对于接口来说是不可能的。TypeScript中的接口是表示类型结构的一种强大方式。它们允许您在记录它们时以类型安全的方式使用这些结构,从而直接改善开发人员的体验。在今天的文章中,我们将在TypeScript中创建接口,学习如何使用它们,并了解普通类型和接口之间的区别。我们将尝试不同的代码示例,这些示例可以在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+语法,例如解构、剩余运算符和导入/导出。如果您需要有关这些主题的更多信息,我们建议您阅读我们的如何使用JavaScript编写代码系列。本教程将涉及支持TypeScript并显示内联错误的文本编辑器的各个方面。这不是使用TypeScript所必需的,但它确实更多地利用了TypeScript的特性。要获得这些好处,您可以使用像VisualStudioCode这样的文本编辑器,它完全支持开箱即用的TypeScript。您还可以在TypeScriptPlayground中尝试这些好处。本教程中显示的所有示例都是使用TypeScript版本4.2.2创建的。在TypeScript中创建命名空间在本节中,我们将学习如何在TypeScript中创建命名空间,以说明一般语法。要创建命名空间,我们将使用命名空间关键字,后跟命名空间的名称,然后是{}块。例如,我们将创建一个DatabaseEntity命名空间来保存数据库实体,就像我们使用对象关系映射(ORM)库一样。将以下代码添加到新的TypeScript文件中:namespaceDatabaseEntity{}这声明了DatabaseEntity命名空间,但尚未向其中添加任何代码。接下来在命名空间中添加一个User类,代表数据库中的一个User实体:为了说明这一点,创建一个新的User实例并将其存储在newUser变量中:namespaceDatabaseEntity{classUser{constructor(publicname:string){}}constnewUser=newUser("Jon");}这是有效代码。但是,如果我们尝试在命名空间之外使用User,TypeScript编译器会给出错误2339:OutputProperty'User'doesnotexistontype'typeofDatabaseEntity'。(2339)如果我们要使用命名空间外的类,必须先将User类导出为外部可用,如下高亮代码所示:namespaceDatabaseEntity{exportclassUser{constructor(publicname:string){}}constnewUser=newUser("Jon");}我们现在可以使用其完全限定名称访问DatabaseEntity命名空间之外的User类。在这种情况下,完全限定名称是DatabaseEntity.User:namespaceDatabaseEntity{exportclassUser{constructor(publicname:string){}}constnewUser=newUser("Jon");}constnewUserOutsideNamespace=newDatabaseEntity.User("Jane");我们可以从命名空间中导出任何东西,包括变量,这些变量将成为命名空间中的属性。在下面的代码中,我们导出了newUser变量:;由于导出了变量newUser,我们可以将其作为命名空间的属性进行访问。运行此代码会将以下内容打印到控制台:OutputJon就像接口一样,TypeScript中的名称空间允许声明合并。这意味着同一命名空间的多个声明将合并为一个声明。如果我们稍后需要在代码中扩展命名空间,这可以提高命名空间的灵活性。使用前面的示例,这意味着如果我们再次声明DatabaseEntity命名空间,我们将能够使用更多属性扩展命名空间。使用另一个命名空间声明将新类UserRole添加到我们的DatabaseEntity命名空间:{constructor(publicuser:User,publicrole:string){}}exportconstnewUserRole=newUserRole(newUser,"admin");}在新的DatabaseEntity命名空间声明中,我们可以使用之前在DatabaseEntity命名空间中导出的Any成员,包括从以前的声明中导出的成员,没有他们的完全限定名称。我们使用第一个命名空间中声明的名称将UserRole构造函数中的用户参数键入为User类型,并在使用newUser值创建新的UserRole实例时使用。这是可能的,因为我们在之前的命名空间声明中导出了这些。现在我们了解了命名空间的基本语法,我们可以继续讨论TypeScript编译器如何将命名空间转换为JavaScript。使用命名空间时检查生成的JavaScript代码TypeScript中的命名空间不仅仅是编译时功能。他们还更改了生成的JavaScript代码。要详细了解命名空间的工作原理,我们可以分析支持此TypeScript功能的JavaScript。在此步骤中,我们将使用上一节中的代码片段并检查它们的底层JavaScript实现。以我们在第一个例子中使用的代码为例:;TypeScript编译器为此TypeScript片段生成以下JavaScript代码:.newUser=newUser("Jon");})(DatabaseEntity||(DatabaseEntity={}));console.log(DatabaseEntity.newUser.name);为了声明DatabaseEntity命名空间,TypeScript编译器创建一个名为DatabaseEntity的未初始化变量,然后创建一个立即调用的函数表达式(IIFE)。这个IIFE接收一个参数DatabaseEntity||(DatabaseEntity={}),这是DatabaseEntity变量的当前值。如果未设置为true,则将变量的值设置为空对象。在将DatabaseEntity的值传递给IIFE时将其值设置为null是可行的,因为赋值操作的返回值是被赋值的值。在本例中,它是空对象。在IIFE内部,创建了User类,然后将其分配给DatabaseEntity对象的User属性。newUser属性也是如此,我们将属性分配给新User实例的值。现在看第二个代码示例,其中有多个名称空间声明:UserRole{constructor(publicuser:User,publicrole:string){}}exportconstnewUserRole=newUserRole(newUser,"admin");}生成的JavaScript代码如下:"usestrict";varDatabaseEntity;(函数(DatabaseEntity){classUser{constructor(name){this.name=name;}}DatabaseEntity.User=User;DatabaseEntity.newUser=newUser(“Jon”);})(DatabaseEntity||(DatabaseEntity={}));(function(DatabaseEntity){classUserRole{constructor(user,role){this.user=user;this.role=role;}}DatabaseEntity.UserRole=UserRole;DatabaseEntity.newUserRole=newUserRole(DatabaseEntity.newUser,“管理员”);})(DatabaseEntity||(DatabaseEntity={}));代码的开头看起来和之前一样,未初始化的变量DatabaseEntity,然后是IIFE,这次实际代码设置DatabaseEntity对象的属性,不过,还有另一个IIFE。这个新的IIFE匹配DatabaseEntity命名空间的第二个声明。现在,当执行第二个IIFE时,DatabaseEntity已经绑定到一个对象,所以我们只需通过添加额外的属性来扩展已经可用的对象。我们现在已经了解了TypeScript命名空间的语法以及它们在底层JavaScript中的工作方式。有了这个上下文,我们现在可以执行命名空间的常见用例:无需键入即可为外部库定义类型。使用命名空间为外部库提供类型在本节中,我们将介绍命名空间有用的场景之一:为外部库创建模块声明。为此,我们将在TypeScript项目中编写一个新文件来声明类型,然后更改tsconfig.json文件以使TypeScript编译器识别这些类型。注意:要执行后续步骤,您需要一个可以访问文件系统的TypeScript环境。如果您使用的是TypeScriptPlayground,则可以通过单击顶部菜单中的导出,然后单击在CodeSandbox中打开,将现有代码导出到CodeSandbox项目。这将允许您创建新文件并编辑tsconfig.json文件。并非npm注册表中的每个可用包都捆绑了自己的TypeScript模块声明。这意味着在您的项目中安装包时,您可能会遇到与包缺少类型声明相关的编译错误,或者必须使用所有类型都设置为any的库。根据您对TypeScript的严格程度,这可能是不希望出现的结果。希望这个包有一个由DefinetelyTyped社区创建的@types包,允许你安装这个包并获得这个库的工作类型。然而,情况并非总是如此,有时您不得不处理一个不捆绑其自身类型模块声明的库。在这种情况下,如果你想让你的代码完全类型安全,你必须自己创建模块声明。例如,假设您正在使用一个名为example-vector3的向量库,它使用一个方法add导出一个类Vector3。此方法用于添加两个Vector3向量。库中的代码可能如下所示:exportclassVector3{super(x,y,z){this.x=x;这个.y=y;这个.z=z;}add(vec){让x=this.x+vector.x;让y=this.y+vector.y;让z=this.z+vector.z;让newVector=newVector3(x,y,z);returnnewVector}}这将导出一个类,该类创建一个向量,其中x、y和z属性表示向量的坐标分量。接下来,看一下使用假设库的示例代码:index.tsimport{Vector3}from"example-vector3";constv1=newVector3(1,2,3);constv2=newVector3(1,2,3);constv3=v1.add(v2);example-vector3库没有绑定自己的类型声明,因此TypeScript编译器会给出错误2307:OutputCannotfindmodule'example-vector3'oritscorrespondingtypedeclarations.ts(2307)为了解决这个问题,我们现在将创建一个此包的类型声明文件。首先,创建一个名为types/example-vector3/index.d.ts的新文件。然后,在您喜欢的编辑器中打开它。在此文件中写入以下代码:declaremodule"example-vector3"{export=vector3;namespacevector3{}}在此代码中,我们正在为example-vector3模块创建类型声明。代码的第一部分是声明模块块本身。TypeScript编译器将解析此块并解释其中的所有内容,就好像它是模块本身的类型表示一样。这意味着无论我们在这里声明什么,TypeScript都将使用它来推断模块的类型。现在,您说此模块导出一个名为vector3的名称空间,该名称空间当前为空。保存并退出该文件。TypeScript编译器目前不知道您的声明文件,因此您必须将其包含在您的tsconfig.json中。为此,通过将types属性添加到compilerOptions选项来编辑项目tsconfig.json:{"compilerOptions":{..."types":["./types/example-vector3/index.d.ts"]}}现在,如果我们回到原来的代码,我们会看到错误发生了变化。TypeScript编译器现在给出错误2305:OutputModule“example-vector3”没有导出成员“Vector3”。ts(2305)当我们为example-vector3创建模块声明时,导出当前设置为空命名空间。Vector3类不是从此命名空间导出的。重新打开types/example-vector3/index.d.ts并写入以下代码:declaremodule"example-vector3"{export=vector3;namespacevector3{exportclassVector3{constructor(x:number,y:number,z:number);添加(vec:Vector3):Vector3;}}}在这段代码中,请注意我们现在如何在vector3命名空间中导出一个类。模块声明的主要目标是为库公开的值提供类型信息。这样,我们就可以以类型安全的方式使用它。在这种情况下,我们知道example-vector3库提供了一个名为Vector3的类,它在构造函数中接受三个数字,并有一个add方法可以添加两个Vector3实例并返回一个新实例作为结果。我们不需要在这里提供实现,只需要类型信息本身。不提供实现的声明在TypeScript中称为环境声明,这些声明通常在.d.ts文件中创建。此代码现在可以正确编译并具有Vector3类的正确类型。使用命名空间,我们可以将库导出的内容隔离到单个类型单元中,在本例中为vector3命名空间。这使得自定义模块声明变得更加容易,甚至可以通过将类型声明提交到DefinetelyTyped存储库来使所有开发人员都可以使用类型声明。最终结论在今天的教程中,我们了解了TypeScript中命名空间的基本语法,并检查了TypeScript编译器将其更改为的JavaScript。我们还尝试了命名空间的一个常见用例:为尚未键入的外部库提供环境类型。尽管不推荐使用命名空间,但并不总是建议使用命名空间作为代码库中的代码组织机制。现代代码应该使用ES模块语法,因为它具有命名空间提供的所有功能,并且自ECMAScript2015以来,它是规范的一部分。但是,在创建模块声明时仍然建议使用命名空间,因为它允许更简洁的类型声明。如果你想阅读更多关于TypeScript的教程文章,请看下面的推荐阅读。如果你觉得我今天的教程还不错,请点个赞,关注我,把这篇文章分享给你的朋友,说不定能帮到他。最后,感谢阅读,祝编程愉快!
