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

TypeScript的高级应用与完美实践

时间:2023-03-13 18:29:52 科技观察

当我们谈论TypeScript时,我们在谈论什么?定位TypeScriptJavaScript的超集编译时行为不会引入额外的开销,也不会改变运行时行为。它始终符合ESMAScript语言标准(第三阶段语法)。TypeScript中的装饰器很特别。它是Angular团队和TypeScript团队交易的结果。有兴趣的可以自行搜索相关资料。而且,最近EcmaScript规范中关于装饰器提案的内容发生了翻天覆地的变化。建议等待此语法标准完全稳定后再用于生产项目。本文只讨论图中蓝色部分。类型的本质是契约JSDoc也可以标注类型,为什么要用TypeScript呢?JSDoc只是注释,其注解没有绑定作用。TS有--checkJs选项,但是不好用。TS会自动推断函数的返回值类型。为什么要把它标出来?合同高于执行。检查返回值是否错误。写回信时得到提醒。几组VSCode快捷键代码补全control+spacectrl+spaceQuickfixcommand+.ctrl+。重构(重命名)fn+f2f2网站TypeScriptPlayground初始化项目配置自己"moduleResolution":"node"}react项目运行create-react-app${projectname}--scripts-version=react-scripts-tssmalltest&and|operators虽然从写法上来说,这两个运算符和按位逻辑运算符是一样的。但在语义上,它们与按位运算完全相反。位运算的性能:1001|1010=1011//Merge11001&1010=1000//只保留total1在TypeScript中的性能:interfaceIA{a:stringb:number}typeTB={b:numberc:number[]}typeTC=IA|TB;//TC类型变量的key只需要包含ab或者bc即可。当然abc也可以有typeTD=IA&TB;//TD类型变量的key必须包含abc。对于这种表现,可以这样理解:&表示必须同时满足多个契约,而|表示意味着可以满足任何契约。interface和type关键字interface和type是两个关键字,因为它们的功能比较接近,经常会引起新手的疑问:什么时候用type,什么时候用interface?接口的特点是:同名接口自动聚合,也可以和已有的同名类聚合。它适用于polyfill。它只能表示对象/类/函数的类型。建议库的开发者提供的公共API尽量使用interface/class,方便用户自行扩展。比如我在开发腾讯云CloudStudio在线编辑插件时,查阅的monaco文档是0.15.5版本(当时最新版本),而CloudStudio使用的monaco版本是0.14。3.我需要的一些API丢失了,所以我需要手动填充它们。/***CloudStudio使用的monaco版本早于0.14.3,和官方文档相比缺少一些功能*另外vscode有一些独特的功能需要适配*所以这里手动实现作为补充*/declaremodulemonaco{interfacePosition{delta(deltaLineNumber?:number,deltaColumn?:number):Position}}//monaco0.15.5monaco.Position.prototype.delta=function(this:monaco.Position,deltaLineNumber=0,deltaColumn=0){returnnewmonaco.Position(this.lineNumber+deltaLineNumber,this.column+deltaColumn);}类型相比interface有以下特点:表达功能更强大,不局限于对象/类/函数来扩展已有的类型,需要创建一个新的类型,名字不能重复支持更复杂的类型操作typeTuple=[number,string];consta:Tuple=[2,'sir'];typeSize='small'|'默认'|'大'|数字;常量:大小=24;基本上接口所表达的所有类型都有其等价的类型表达。但是在实践的过程中,我也发现一个类型只能用接口来表达,不能用类型来表达,也就是给函数挂载属性。interfaceFuncWithAttachment{(param:string):boolean;someProperty:number;}consttestFunc:FuncWithAttachment=...;constresult=testFunc('mike');//类型提醒testFunc.someProperty=3;//类型提醒extendskey原来extends这个词的意思是“扩展”,也有人称之为“继承”。在TypeScript中,extends可以用作扩展现有类型的动词;它也可以用作形容词来有条件地限制类型(例如,在泛型中)。扩展现有类型时,不允许类型冲突的覆盖操作。比如基础类型中的keya是string,在扩展类型中不能改成number。typeA={a:number}interfaceABextendsA{b:string}//等价于前面的typeTAB=A&{b:string}泛型我们已经看到,类型实际上可以执行某些操作。如果我们想写的类型更适用,我们让它像函数一样接受参数。TS的泛型就起到了这样的作用,你可以把它当作类型参数来使用。像函数参数一样,它可以有一个默认值。另外,还可以使用extends来限制参数本身需要满足的条件。定义函数、类型、接口或类时,在名称后加<>表示接受类型参数。在实际调用中,不一定需要手动传入类型参数,TS往往可以自行推断。当TS推理不准确时,手动传入参数修正。//定义classReact.Component{...}interfaceIShowConfig{...}//调用classModalextendsReact.Component{...}条件类型除了AND、OR等基本逻辑,TS类型还支持条件运算,其语法与三元运算符相同,即TextendsU?X:Y。这是一个简单的例子。在下文中,我们将看到许多复杂类型的实现需要条件类型的帮助。typeIsEqualType=AextendsB?(BextendsA?true:false):false;typeNumberEqualsToString=IsEqualType;//falsetypeNumberEqualsToNumber=IsEqualType;//trueenvironmentAmbientModules在实际应用开发中有是在当前作用域内可以访问到一个变量,但是这个变量不受开发者控制的场景。比如通过Script标签直接导入的第三方库CDN,一些托管环境的API等,这时候可以使用TS的环境声明功能告诉TS当前作用域可以访问这些变量,从而获取类型提示.具体有两种方式,declare和tripleslashinstructions。declareconstIS_MOBILE=true;//编译后这一行消失constwording=IS_MOBILE?'手机':'PC';整个类型声明文件可以用三重斜杠命令一次导入。///constrange=newmonaco.Range(2,3,6,7);深入类型系统基本类型基本类型也可以理解为原子类型。包括number,boolean,string,null,undefined,function,array,literal(true,false,1,2,'a')等,不能再细分。复合类型TypeScript中的复合类型可以分为两类:集合和映射。set指的是一个无序的、不重复的元素集合。map和JS中的对象一样,是一些没有重复键的键值对。//settypeSize='small'|'default'|'big'|'large';//mapinterfaceIA{a:stringb:number}复合类型之间的转换//map=>settypeIAKeys=keyofIA;//'a'|'b'typeIAValues=IA[keyofIA];//string|number//set=>maptypeSizeMap={[kinSize]:number}//相当于typeSizeMap2={small:numberdefault:numberbig:numberlarge:number}map操作//索引值typeSubA=IA['a'];//string//属性修饰符typePerson={age:numberreadonlyname:string//只读属性,初始化时必须赋昵称?:string//可选属性,相当于|undefined}映射类型和同态变换在TypeScript中,有以下几种常见的映射类型。它们的共同点是只接受一种传入类型。生成类型中的键全部来自keyof传入的类型,值都是传入值的变体typePartial={[PinkeyofT]?:T[P]}//使全部attributesofamapoptionaltypeRequired={[PinkeyofT]-?:T[P]}//使地图所有属性变为必填typeReadonly={readonly[PinkeyofT]:T[P]}//改变allattributesofamapintoread-onlytypeMutable={-readonly[PinkeyofT]:T[P]}//ts标准库中不包含这种使map的所有属性都可写的转换,称为TS中的同态变换。在进行同态变换时,TS会先复制传入参数的属性修饰符,然后应用定义的变换。interfaceFruit{readonlyname:stringsize:number}typePF=Partial;//PF.name为只读可选,PF.size仅对setmaptypeRecord生成的其他常用工具类型可选{[PinK]:T};typeSize='small'|'default'|'big';/*{small:numberdefault:numberbig:number}*/typeSizeMap=Record;保留地图的一部分删除部分地图typeOmit=Pick>;/*{default:number}*/typeDefaultSizeMap=Omit;保留部分集合typeExtract=TextendsU?T:never;typeResult=1|2|3|'error'|'success';typeStringResult=Extract;//'error'|'successdeletepartofsettypeExclude=TextendsU?never:T;typeNumericResult=Exclude;//1|2|3得到函数返回值的类型。但是要注意不要滥用这个工具类型,你应该手动标记尽可能多的函数返回值类型。原因如开头所说,合约高于变现。使用ReturnType是从实现中逆向契约,而实现往往易变易错,而契约相对稳定。另一方面,过多的ReturnType也会降低代码的可读性。typeReturnType=Textends(...args:any[])=>inferR?R:any;functionf(){return{a:3,b:2};}/*{a:numberb:number}*/typeFReturn=返回类型;以上工具类型已经包含在TS标准库中,您可以直接在应用中输入名称使用。另外,在这些工具类型的实现中,会出现infer、never、typeof等关键字,后面会详细解释它们的作用。TyperecursionTSnativeReadonly只限制了一层写操作,我们可以用递归来实现deepReadonly。但需要注意的是,TS对最大递归层数有限制,最多5个递归层。typeDeepReadony={readonly[PinkeyofT]:DeepReadony}interfaceSomeObject{a:{b:{c:number;};};}constobj:Readonly={a:{b:{c:2}}};obj.a.b.c=3;//TS不会报错constobj2:DeepReadony={a:{b:{c:2}}};obj2.a.b.c=3;//无法分配给“c”,因为它是只读属性。从不推断类型的关键字never是|的酉项操作,即x|从不=x。比如前面的Exclude操作过程是这样的:infer的作用是让TypeScript自己进行推断,并将推断出的结果存储在一个临时名称中,只能在extends语句中使用。它和泛型的区别在于,泛型声明的是一个“参数”,而infer声明的是一个“中间变量”。我用的比较少,所以这里有一个官方的例子。typeUnpacked=Textends(inferU)[]?U:Textends(...args:any[])=>inferU?U:TextendsPromise?U:T;typeT0=Unpacked;//stringtypeT1=Unpacked;//stringtypeT2=Unpacked<()=>string>;//stringtypeT3=Unpacked>;//stringtypeT4=Unpacked[]>;//PromisetypeT5=Unpacked[]>>;//stringtypeof用于获取一个“常量”类型,这里的“常量”是指任何可以在编译时确定的东西,比如const,函数、类等。它是从实际运行代码到类型系统的单向通道。理论上,任何想要被类型系统使用的运行时符号名都必须加上typeof。但是类比较特殊,不需要添加,因为ts的类比js的类出现的早,现有的就是兼容方案。使用class时,类名表示实例类型,typeofclass表示类本身的类型。没错,这个关键字和js中的typeof关键字同名:)。constconfig={width:2,height:2};functiongetLength(str:string){returnstr.length;}typeTConfig=typeofconfig;//{width:number,height:number}typeTGetLength=typeofgetLength;//(str:string)=>number实战我在项目中遇到过这样一个场景,需要获取一个类型中所有值为指定类型的key。例如,给定某个React组件的props类型,我需要“知道”(以编程方式)哪些参数是函数类型。interfaceSomeProps{a:stringb:numberc:(e:MouseEvent)=>voidd:(e:TouchEvent)=>void}//如何获取'c'|'d'?分析这里的思路,我们需要从一个map中得到一个集合,这个集合就是map的key的一个子集,过滤子集的条件就是value的类型。要构造集合的子集,您需要使用never;实现条件判断,需要用到extends;而要实现键到值的访问,就需要用到索引值。经过一番尝试,解决方案如下。typeGetKeyByValueType={[KinkeyofT]:T[K]extendsCondition?K:never}[keyofT];typeFunctionPropNames=GetKeyByValueType;//'c'|'d'这里的计算过程如下如下://开始{a:stringb:numberc:(e:MouseEvent)=>voidd:(e:TouchEvent)=>void}//第一步,条件映射{a:neverb:neverc:'c'd:'d'}//第二步,索引值never|never|'c'|'d'//never的性质'c'|'d'CompilerhintsCompilerHintsTypeScript只在编译时出现,所以我们可以在代码中加入一些符号,给编译器一些提示,让它按照我们想要的方式运行。类型转换类型转换的语法为<类型名称>xxx或xxx作为类型名称。建议始终使用as语法,因为第一种语法不能在tsx文件中使用,而且很容易与泛型混淆。一般只有这些场景需要使用类型转换:自动推断不准确;TS报错,想不出更好的打字方式,于是使用手动快捷键;暂时的“自我自由”。在使用类型转换时,要遵守几个原则:如果要放宽限制,只能放宽到能运行的最严格的类型。如果你不知道一个变量的精确类型,最好只用一个近似的类型来标记它(比如any[])any好吧,“自由自在”中的任何一段代码(没有类型覆盖在all)区不要超过2行,在第一个可以确定类型的变量出现时标出。在编写TS程序时,我们的目标是使类型覆盖率无限接近100%。!断言!作用是断言一个变量不会为null/undefined,告诉编译器停止报错。这里由用户来确保断言是正确的。它不同于刚刚进入EcmaScript语法提案第3阶段的可选链接功能。OptionalChaining特性可以保证访问的安全性,即使在undefined上访问一个key,也不会抛出异常。和!只是消除了编译器错误,不会对运行时行为产生任何影响。//TypeScriptmightBeUndefined!.a=2//编译为mightBeUndefined.a=2//@ts-ignore用于忽略下一行的报错,尽量少用。为什么我不提enumenum更早出现在TS中。它引入了JavaScript没有的数据结构(编译成双向映射),侵入运行时,与TypeScript的目的不符。stringliteralunion('small'|'big'|'large')也可以做同样的事情,但是在调试的时候可读性更好。如果你关心条件比较的性能,你应该使用二进制标志加位操作。//TypeScriptenumSize{small=3,big,large}consta:Size=Size.large;//5//编译为varSize;(function(Size){Size[Size["small"]=3]="small";Size[Size["big"]=4]="big";Size[Size["large"]=5]="large";})(Size||(Size={}));consta=Size.large;//5到底应该用什么心态来写TypeScript呢?我们应该用类型系统编写JavaScript,而不是可以编译成JavaScript的Java/C#。任何TypeScript程序在手动删除type部分并将后缀更改为.js后应该可以正常运行。