当前位置: 首页 > 科技观察

TypeScript深水区:三种类型源和三种模块语法

时间:2023-03-12 12:55:39 科技观察

TypeScript为JavaScript添加了一套类型语法。我们在声明变量的时候,可以给变量加上类型信息,这样编译阶段就可以检查出变量的正确用法。不,那是类型检查。给变量加上类型,很自然的会想到在声明的时候指定:比如object:interfacePerson{name:string;age?:number;}constguang:Person={name:'guang'}比如function:functionadd(num1:number,num2:number):number{returnnum1+num2;}这样,在使用它们的时候,比如变量赋值和函数调用,可以通过类型信息来检查使用是否正确:TypeScript设计的类型语法像这样是没有问题的,但那还不够。我们自己写的代码可以这样声明类型,但是如果我们不写呢?例如JS引擎提供的Number、String、Date、RegExp,浏览器环境中的HTMLElement、Event等API。这些API是执行引擎内置的实现,但是我们在代码中会用到它们,同时我们也需要检查它们是否被正确使用,也就是类型检查。如何向这些API添加类型?TypeScript类型声明的三种来源TypeScript设计了??declare的语法,可以单独声明变量的类型:如object:interfacePerson{name:string;age?:number;}declareconstguang:Person;如函数:declarefunctionadd(num1:number,num2:number):number;这样就可以单独声明类型,使用这些API时也可以进行类型检查。像JS引擎这样的API,浏览器提供的API,基本都是必须的,也是标准的。所以TypeScript为它们内置了类型声明。TypeScript包下有一个lib目录,里面有一堆lib.xx.d.ts类型声明文件,是TS内置的一些类型声明。因为这些只是声明类型,没有具体的JS实现,所以TS设计了一个单独的文件类型,即d.ts,其中d的意思是declare。比如lib.dom.d.ts中的类型声明:因为是ts自带的,配置好之后就可以使用了:在tsconfig.json中配置compilerOptions.lib,然后相应的d.ts类型声明文件即可被导入。可能有同学会说,但是内置的类型声明不多,只有dom和es。确实,因为JSAPI和浏览器API都有标准,自然要按照标准来定义类型。其他环境的API可能没有标准,经常变化,自然无法内置,比如node.所以lib中只有dom和es类型声明。如何配置node环境下的类型声明和其他环境下的内置api?node等环境下的API因为没有标准,TS没有内置,但是TS也支持在这些环境下配置类型声明。方式是通过@types/xxx包:TS会先加载内置lib的类型声明,然后在@types包下搜索类型声明。这样,其他环境的类型声明也可以通过这种方式扩展。@types包在DefinitelyTyped项目下管理。如果你想创建一个@types包,你必须阅读他们的文档。一般来说,很快就可以发送到npm:我们知道TS的内置libs是可以配置的,这些扩展的@types/xx包自然也可以配置:可以指定加载@types目录下的哪些包,也可以修改目录找到@types包(默认是node_modules/@types):@types包除了在node等环境中为API添加类型声明外,还有一个用途,就是添加一些JS包的类型声明:如果代码本身是用ts写的,可以在编译时开启compilerOptions.declaration生成d.ts文件:然后在package.json中配置types指定dts的位置,这样就不用'需要一个单独的@types打包。但是如果代码不是用ts写的,可能需要单独写一个@types/xxx包来声明ts类型,然后在tsconfig.json中配置并加载。比如常用的vue3就不需要@types/vue包,因为它是用ts写的,npm包里也有dts文件。但是react不是ts写的,而是用facebook自己的flow,自然需要@types/react包来添加ts类型声明。至此,我们知道了如何加载ts内置的dom和es的类型声明,以及其他环境下一些包的类型声明。自己写的ts代码呢?其实我们经常配置这些,也就是配置编译好的入口文件,通过include指定一堆,然后通过excludes去掉一部分。也可以通过文件单独包含一些:tsc编译时会分别加载lib、@types、include和files文件进行类型检查。这就是ts类型声明的三个来源。现在还有一个问题。有的API是全局的,有的API属于某个模块。ts如何声明全局API的类型,如何声明模块中API的类型?全局类型声明vs模块类型声明我们写的JS代码是有的API是全局的,有的API是在模块中的,所以TS需要支持这个很正常。但是JS模块规范一开始是不存在的。一开始是通过全局挂一个对象,然后在这个对象上挂一些API,也就是namespace命名空间。所以TS最早支持的模块化方案自然是namespace:namespaceGuang{exportinterfacePerson{name:string;年龄?:数字;}constname='guang';常量年龄=20;exportconstguang:Person={name,age}exportfunctionadd(a:number,b:number):number{返回a+b;}}懂namespace的可以看看编译后的代码:就是全局放一个对象,然后在对象暴露的属性上多挂几个。看完编译后的代码,是不是瞬间学会了命名空间?后来出现了CommonJS规范,不能再叫namespace了,所以TS支持modules。很容易认为@types/node的api定义只是一堆模块:这个模块和命名空间有什么区别?其实并没有什么区别,只是module后面通常是一个路径,而namespace的后半部分是一个命名空间名。其余语法相同。而这个结论是有根据的:用astexplorer.net看解析出来的AST,两者的AST类型是一样的。也就是说编译器后面的处理都是一样的,不是一个东西。后来的故事大家都知道了。JS有es模块规范,所以现在推荐直接使用importexport来声明模块和导入导出。extra的很多,但是有一个导入类型的语法,可以单独导入类型:importtype{xxx}from'yyy';所以现在不建议在声明模块的时候使用namespace和module,还是尽量使用es模块。全局类型声明呢?有了es模块,TS有了单独的设计:在dts中,如果没有import和export语法,所有的类型声明都是全局的,否则都在模块内部。试试看:include在src下配置ts文件,然后使用files导入global.d.ts文件:在global.d.ts中声明一个func函数:在src/index.ts中有提示:compileNo错误:添加import语句:编译报错,说找不到func:这说明func已经不是全局类型了。这时候可以手动声明global:再试一次,编译通过:不仅可以在es模块module中声明global类型,也可以在module方式中声明CommonJS模块:比如有@types/node这些全局类型声明中的相当一部分:这是在typescript中声明模块的3种语法,以及如何声明全局类型。那么如果需要导入模块,同时又需要全局声明类型,有没有更好的方法呢?是的,通过编译器指令参考。这样就可以引入类型声明,而不会导致所有的类型声明都变成in-module:可以看到很多dts都是这样导入其他dts,只是为了保证导入的类型声明仍然是全局的:SummarizeTypeScripttoJavaScriptAdded类型信息以在编译时进行类型检查。除了在声明变量的时候定义类型,TS还支持通过declare单独声明类型。只存放类型声明的文件后缀为d.ts。TypeScript有三个地方存放类型声明:lib:内置类型声明,包括dom和es,因为它们都有标准。@types/xx:其他环境的API类型声明,比如node,npm包的类型声明。开发者编写的代码:由include+excludeandfiles指定。其中,npm包还可以同时存储ts类型,只需通过packages.json的types字段指定路径即可。常见的是vue的类型存放在npm包下,而react的类型存放在@types/react。因为一个源码是ts写的,一个不是。巧合的是,TS也有三种声明模块的方式:命名空间:最早实现模块的方式,编译成JS代码用于声明对象和设置对象的属性,很容易理解。module:和命名空间的AST没什么区别,但一般用于声明CommonJS模块,@types/node下有很多。es模块:es标准模块语法,ts额外扩展了导入类型。dts的类型声明默认是global,除非有es模块的import和export的声明,这时候需要手动声明global。为避免这种情况,请使用参考编译器指令。如果你对TypeScript的掌握很深,除了学习类型定义和类型编程,这三种声明类型(lib、@types、用户目录)的来源,以及模块声明的三种类型(namespace、module、es)module),还有全局类型的声明(global,reference)也要掌握。