当前位置: 首页 > 后端技术 > Node.js

精读《Typescript2.0 - 2.9》

时间:2023-04-03 15:30:33 Node.js

1简介精读原文为typescript2.0-2.9的文档:2.0-2.8,2.9草稿。我发现很多写了一年多的Typescript开发者在理解和使用Typescript上还处于入门阶段。出现这种现象的原因是Typescript知识的积累需要刻意练习,使??用Typescript所花费的时间与对它的理解程度关系不大。本文选取了TS在2.0-2.9版本中最为重要的功能,并结合实际案例进行解读,帮助您快速跟上TS的更新节奏。TS内部优化中用户不敏感的部分就不一一列举了,因为这些优化在日常使用中都能感受到。2精读由于Typescript在严格模式下的很多表现与非严格模式下的表现不同,为了避免不必要的记忆,建议只记住严格模式!解决了严格模式导致的大量边界检测代码。直接访问变量的属性时,如果变量是undefined,不仅访问不到属性,js还会抛出异常,这几乎是业务开发中最常见的。报错了(往往是后端数据异常导致的),typescript的严格模式会检查这种情况,不允许出现不安全的代码。在2.0版本中,提供了一个“非空断言标识符”!。解决不会报错的情况,比如配置文件是静态的,肯定不会抛出异常,但是在2.0之前的版本,我们可能要这样调用Object:constconfig={port:8000};if(config){console.log(config.port);}利用2.0提供的“非空断言标识符”,我们可以这样写:console.log(config!.port);在2.8版本中,ts支持条件类型语法:typeTypeName=Textendsstring?"string"当T的类型为string时,TypeName的表达式类型为"string"。这时可以构造一个自动的“非空断言”类型,代码可以简化为:console.log(config.port);前提是框架先指定config为这个特殊类型,这个特殊类型的定义如下:exporttypePowerPartial={[UinkeyofT]?:T[U]extendsobject?PowerPartial:T[U]};即2.8的条件类型允许我们在类型判断中递归,把所有的对象键都加一层“非空断言”!这里的灵感来源于egg-ts的总结。添加了never对象类型。当一个函数无法执行,或者理解为中途中断时,TS2.0认为它是never类型。例如,throwError或while(true)将导致函数返回值类型为never。与nullundefined属性一样,在函数的返回值中never等于null或undefined。它们都是亚型。例如类型number有null和undefined两种子类型,因为任何类型化的值都可能为空(即在执行过程中可能没有值)。这里涉及到一个很重要的概念,即预定义类型并不意味着类型一定要符合预期,就像函数运行时可能会被throwError中断一样。所以为了应对这种情况,ts将nullundefined设置为所有类型的子类型,从2.0开始,函数的返回值类型never多了一个子类型。TS2.2支持object类型,但是很多时候我们总是把object和anytype混淆,比如下面的代码:constpersion:object={age:5};控制台日志(persion.age);//错误:属性“age”在类型“object”上不存在。这时候会出现错误,有时候闭上眼睛改成any就大功告成了。其实这个时候只要把对象删掉,换成TS的自动推导就可以了。那么问题是什么?首先,object不是这样使用的。它是在TS2.3中加入的,用来描述一个非基本类型,所以一般用于类型验证,比如作为参数类型。如果参数类型为object,则允许传入任何对象数据,但不允许传入非对象类型如3"abc":declarefunctioncreate(o:object|null):void;create({prop:0});//正确的create(null);//正确创建(42);//不正确的create("string");//不正确的创建(false);//不正确的创建(未定义);//incorrectandinitiallyconstpersion:object的使用将可以准确推导出的对象类型扩展为整体的、模糊的对象类型。TS自然无法推断出这个对象有哪些键,因为对象类型只表示它是一个对象类型,对象整体观察时是真的,但是对象类型不识别任何具体的键。2.0版本新增修饰类型TS,支持readonly修饰符,被其修饰的变量不可修改。在TS2.8中,添加了-和+修饰符,有点像形容词的副词。比如readonly就是+readonly,我们也可以用-readonly去掉只读特性;我们也可以通过-?:去掉可选类型,这样我们就可以扩展一个新的类型:Required,去掉对象所有的可选修饰,自然就变成了必选类型:typeRequired={[PinkeyofT]-?:T[P]};可以定义函数的this类型也在TS2.0版本中,我们可以自定义this的类型,这在vue框架中特别有用:functionf(this:void){//确保`this`在this中不可用standalonefunction}这个类型是假参数,所以不会影响函数真正参数的个数和位置,而是定义在参数位置上,永远排在队列的最前面。引用和寻址支持通配符简单来说,模块名可以用*来表示任意单词:declaremodule"*!text"{constcontent:string;exportdefaultcontent;}它的类型可以辐射到:importfileContentfrom"./xyz.txt!text";这个特性最强大的特性之一是它被用在扩展模块中,因为包括tsconfig.json在内的模块搜索也支持通配符!举个例子来理解:最近流行的umi框架有一个locale插件,只要安装这个插件,就可以从umi/locale获取国际化的内容:import{locale}from"umi/locale";其实它的实现是创建一个文件,通过webpack.alias指向引用。这种做法很好,那么如何为它添加类型支持呢?只需像这样配置tsconfig.json:{"compilerOptions":{"paths":{"umi/*":["umi",""]}}}将所有umi/*类型指向,那么umi/locale会指向文件/locale.ts。如果插件自动创建的文件名也叫locale.ts,那么类型会自动对应。跳过仓库类型错误TS在2.x中支持了很多新的compileOptions,但是skipLibCheck实在是太眼花缭乱了,不得不单独提一下。skipLibCheck属性不仅可以忽略npm不规范导致的报错,还可以最大程度的支持类型系统,可以说是一箭双雕。以某一个UI库为例。某天发布的小版本的d.ts文件有bug,导致整个项目无法构建。您不再需要提交PR来敦促作者修复它!skipLibCheck可以忽略这个错误,同时保持类型的自动推导,这意味着它比declaremodule"ui-lib"settingthetypetoany更强大。增强类型修改TS2.1版本可谓是类型操作的革命性版本,我们可以通过keyof获取对象key的类型:interfacePerson{name:string;age:number;}typeK1=keyofPerson;//"名称"|“age”是基于keyof,我们可以增强对象的类型:typeNewObjType={[PinkeyofT]:T[P]};Tips:在TS2.8版本中,我们可以使用表达式作为keyof参数,比如keyof(A&B)。Tips:在TS2.9中,keyof可能会返回非字符串值,所以不要一开始就认为keyof的返回类型一定是string。NewObjType原封不动地重新描述了对象类型,这似乎没有意义。但实际上,我们有3个地方可以扩展:左边:比如对象的属性可以通过修改为readonly,使其变为只读。中间:例如,将:更改为?:以使对象的所有属性成为可选的。右图:比如设置一层Promise覆盖对象每个键的值类型。基于这些能力,我们在上层开发了一系列有用的接口:Readonly。将所有对象键设置为只读,或者使用2.8的条件类型语法实现递归设置只读。部分。将对象的键设置为可选。选择。从对象类型T中挑选一些属性K,比如对象有10个key,只需要将K设置为"name"|"age"生成一个只支持这两个键的新对象类型。提取。它是Pick的底层API,直到2.8版本才内置。可以认为Pick是选中对象的某个key,Extract是选中key的key。记录。将对象的某些属性转换为另一种类型。更常用于回调场景。回调函数返回的类型将覆盖对象的每个键的类型。这时候类型系统就需要Record接口来完成推导。排除。排除T中的U类型,与Extract的作用相反。省略(非内置)。从对象T中排除键是K的属性。使用内置类型可以轻松推导出:typeOmit=Pick>NonNullable。消除了T为null和未定义的可能性。返回类型。获取函数T的返回值类型,很有意义。实例类型。获取构造函数类型的实例类型。以上类型都内置在lib.d.ts中,不需要定义就可以直接使用,可以认为是Typescript的utils工具库。单独以ReturnType为例来说明其重要性:Redux中Connect的第一个参数是mapStateToProps,这些Props会自动与ReactProps聚合。我们可以使用ReturnType来获取注入到Props中的当前Connect的类型。您可以通过Connect和React组件的类型系统。Generators的类型定义和async/awaitTS2.3版本对Generators做了很多增强,但实际上我们已经将其替换为async/await,所以TS对Generators的增强可以忽略。需要注意的一点是对for..of语法的异步迭代支持:asyncfunctionf(){forawait(constxoffn1()){console.log(x);}}这会为每个步骤启用异步迭代。注意下面的写法:asyncfunctionf(){for(constxofawaitfn2()){console.log(x);}}对于fn1,它的返回值是一个可迭代对象,每个item类型都是Promise或者Generator。对于fn2来说,它本身就是一个异步函数,返回值是可迭代的,每一项都不是异步的。例如:functionfn1(){return[Promise.resolve(1),Promise.resolve(2)];}functionfn2(){return[1,2];}对了,对于Array.map的方法异步等待每个项目:awaitPromise.all(arr.map(asyncitem=>{returnawaititem.run();}));如果是为了执行顺序,可以换成for..of的语法,因为数组类型是可迭代类型。Genericdefaultparameters理解这个之前,先介绍下TS2.0之前支持的函数类型重载。首先,JS不支持方法重载,而Java支持,而TS类型系统在一定程度上是对标Java的,当然要支持这个功能。幸运的是,JS有一些补救措施来实现伪方法重载,典型的是redux的createStore:"){增强器=preloadedState;预加载状态=未定义;}}由于JS有办法支持方法重载,TS补充了函数类型重载。两者结合相当于Java方法重载:declarefunctioncreateStore(reducer:Reducer,preloadedState:PreloadedState,enhancer:Enhancer);declarefunctioncreateStore(reducer:Reducer,enhancer:Enhancer);可以明显看出createStore是想表达参数个数的重载。如果定义了函数类型重载,TS会根据函数类型自动判断对应于哪个定义。不过TS2.3支持泛型默认参数,在某些场景下可以减少函数类型重载的代码量,比如下面的代码:declarefunctioncreate():Container;declarefunctioncreate(element:T):Container;声明函数create(element:T,children:U[]):Container;通过枚举的方式表达泛型的默认值以及U和T之间可能存在的关系,可以用泛型的默认参数来解决:declarefunctioncreate(element?:T,children?:U):Container;尤其是在使用React的过程中,如果Component定义了一个通用的默认值:..Component。.可以实现如下等价效果:classComponentextendsReact.PureComponent{//...}//等价于classComponentextendsReact.PureComponent{//...}DynamicImportTSfrom2.4版本开始支持动态导入,Webpack4.0也支持这种语法(详见精读《webpack4.0%20 升级指南》),生产环境可以正式使用这种语法:constzipUtil=awaitimport(./utils/create-zip)-文件”);准确的说,动态导入是在webpack2.1.0-beta.28中实现的,最终在TS2.4版本中得到了语法支持。从TS2.9开始,支持import()类型定义:constzipUtil:typeofimport('./utils/create-zip-file')=awaitimport('./utils/create-zip-file')即typeof可以在不实际导入js内容的情况下作用于import()语法。但是需要注意的是,这个import('./utils/create-zip-file')路径需要推导,比如这个npm模块是否存在,相对路径,或者tsconfig.json中定义的路径。幸运的是,import语法本身限制了路径为字面值,这使得自动推导的成功率非常高。只要是正确的代码,几乎可以肯定推导出来。嗯,所以这也从另一个角度建议大家放弃require。枚举类型支持字符串从Typescript2.4开始,支持枚举类型使用字符串作为值:enumColors{Red="RED",Green="GREEN",Blue="BLUE"}作者提醒我这个函数可能不是在纯前端代码中很有用。因为建议在TS中所有enum的地方都使用enum接收,下面给出一个例子://Correct{type:monaco.languages.types.Folder;}//Error{type:75;}不仅仅是可读性,enum对应的数字可能会变化,直接写75是有风险的。但是如果前端和前端之间有交互,前端是不可能发送enum对象的,必须转成数字。这时候使用字符串作为值比较安全:enumtypes{Folder="FOLDER"}fetch(`/api?type=${monaco.languages.types.Folder}`);数组类型可以指定长度。最典型的就是图表图,往往是这样的二维数组数据类型:[[1,5.5],[2,3.7],[3,2.0],[4,5.9],[5,3.9]]一般我们会这样描述它的数据结构:constdata:string[][]=[[1,5.5],[2,3.7],[3,2.0],[4,5.9],[5,3.9]];在TS2.7中,我们可以更准确地描述每一项的类型和数组的总长度:interfaceChartDataextendsArray{0:number;1:数量;length:2;}自动类型推导自动类型推导有两种类型,即typeof:functionfoo(x:string|number){if(typeofx==="string"){returnx;//字符串}返回x;//number}andinstanceof:functionf1(x:B|C|D){if(xinstanceofB){x;//B}elseif(xinstanceofC){x;//C}else{x;//D}}在TS2.7中,增加了in的推导:interfaceA{a:number;}interfaceB{b:string;}functionfoo(x:A|B){if("a"inx){返回x.a;}returnx.b;}这样就解决了自动推导对象类型的问题,因为object既不能用keyof也不能用instanceof来判断类型,所以找到对象的特征,再也不会像以前那样用了://Badfunctionfoo(x:A|B){//我知道它是A,但我无法描述它。(xasA).keyofA;}//Goodfunctionfoo(x:A|B){//我知道它是A,因为它有属性`keyofA`if("keyofA"inx){x.keyofA;}}4小结整体看完了Typescript2.0-2.9文档,可以看出连贯性还是很强的,但是我们可能不习惯按部就班的学习新语法,因为新语法需要时间来digest,同时需要结合前面语法的上下文才能更好的理解。因此,本文从功能而非版本的角度来梳理TS的新特性,更符合学习习惯。另一个感悟是,我们可能要用追月漫的思维去学习新的语言,尤其是TS这种发展迭代很快的语言。5更多讨论讨论地址为:精读《Typescript2.0 - 2.9》·第85期·dt-fe/weekly想参与讨论的请点这里,每周都有新话题,周末或周一发布。