这六个月,我陆续交了几个同事的代码,发现虽然使用了严格的eslint来规范代码的写法,而且项目中也使用了Typescript满满的,但是在review代码的过程中,还是有很多不整洁、不规范的地方。好的代码可读性强,易于维护,并且不易重构。本文将结合Typescript谈谈如何清洗代码:基本规范函数式1.基本规范(1)常量必须有命名,逻辑判断时,不允许直接与未命名的常量进行比较。写错了switch(num){case1:...case3:...case7:...}if(x===0){...}上面的例子,不知道是什么137对应的是什么意思,这种写法基本是不可读的。enumDayEnum{oneDay=1,threeDay=3,oneWeek=7,}letnum=1;switch(num){caseDayEnum.oneDay:...caseDayEnum.threeDay:...caseDayEnum.oneWeek:...的正确写法}constRightCode=0;if(x===RightCode)从上面的正确写法可以看出,常量是有名字的。在做switch或者if等逻辑判断的时候,我们可以从变量名中知道常量的具体含义,增加了可读性的可能性。(2)枚举除了常量枚举,在Typescript的编译阶段,枚举会生成一个映射对象,如果不是字符串枚举,甚至会生成一个双向映射。所以在我们的业务代码中,有了枚举,就不需要和枚举值相关的数组了。错误的写法enumFruitEnum{tomato=1,banana=2,apple=3}constFruitList=[{key:1,value:'tomato'},{key:2,value:'banana'},{key:3,value:'apple'}]这里错误的原因是冗余。得到一个FruitList,我们不需要新建一个,直接根据FruitEnum的枚举生成一个数组即可。其原理就是我们之前提到的Typescript的枚举,比如除了常量枚举外,在编译时还会生成一个map对象。enumFruitEnum{tomato=1,banana=2,apple=3}constFruitList=Object.entries(FruitEnum)的正确写法才是正确的写法。这种写法不仅不冗余,而且如果修改了枚举的类型,我们只需要直接修改枚举,那么派生数组也会发生变化。另外,字符串枚举值和字符串有本质区别。定义类型时请注意,否则你写的代码会很冗余。使用错误[bob.genderaskeyoftypeofGenderEnum]}出现上述错误的原因是在IPerson的类型定义中,gender不应该是一个字符串,而是一个枚举键。因此,在将字符串转换为枚举值时,必须添加一个断言askeyoftypeofGenderEnum。正确的写法enumGenderEnum{'male'='boy','female'='girl'}interfaceIPerson{name:stringgender:keyoftypeofGenderEnum}letbob:IPerson={name:"bob",gender:'male'}{Gender[bob.gender]}以上是正确的写法。字符串枚举和字符串类型之间有明显的区别。当变量需要使用枚举时,不能定义为字符串。(3)ts-ignore&anyTypescript应严格禁止使用ts-ignore,ts-ignore是影响Typescript代码质量的最重要因素。对于any,我曾经想在我的项目中禁用any,但是有一些场景是需要用到any的,所以并没有粗暴的禁止any的使用。但在大多数情况下,您可能不需要使用任何。需要用到any的场景可以具体情况具体分析。ts-ignore使用错误的场景//@ts-ignoreimportPluginfrom'someModule'//如果someModule的语句不存在Plugin.test("helloworld")以上是最经典的ts-ignore使用场景,上面方法使用ts-ignore。那么Typescript就会认为Plugin的类型是any。正确的方法是通过declaremodule的方法自定义需要使用的类型。正确的方法importPluginfrom'someModule'declaremodule'someModule'{exporttypetest=(arg:string)=>void;}可以定义模块内部的声明,同名声明遵循一定的合并原则,如果要扩展三方模块,声明模块非常方便。在大多数相同的场景中,您不需要使用任何。在某些场景下,如果不能立即确定一个值的类型,我们可以使用unknown代替any。any会完全失去类型判断,这本身其实是相当危险的,而使用any就相当于放弃了类型检测,基本放弃了typescript。例如:letfish:any={type:'animal',swim:()=>{}}fish.run()在上面的例子中,我们调用了一个不存在的方法,因为使用了any,所以是skipped没有静态类型检查,是不安全的。运行时会有错误。如果不能立即确定值的类型,我们可以使用unknown而不是使用any。letfish:unknown={type:'animal',swim:()=>{}}fish.run()//会报错unkonwn是任意类型的子类型,所以和any一样,任意类型都可以赋值不知道。与any不同的是,unkonwn变量必须定义自己的类型,只有经过类型收缩或类型断言后,unkonwn变量才能正常使用定义在其上的方法和变量。简单来说,unkonwn在使用前需要强行判断其类型。(4)namespaceTypescript代码,尤其是面向业务的开发,基本上不用namespace。另外,在nodejs中天然支持module,而在es6(next)中,esmodule也成为了语言级别的规范,所以Typescript官方推荐使用module。简单的说,命名空间就是一个全局对象。当然我们也可以把namespace放在module里面,但是把namespace放在module里面也是有问题的。错误的方式//UsingexportnamespaceShapes{exportclassTriangle{/*...*/}exportclassSquare{/*...*/}}inashapes.tsmodule//当我们使用shapes.ts//shapeConsumer.tsimport*asshapesfrom"./shapes";lett=newshapes.Shapes.Triangle();//shapes.Shapes?正确的做法(直接使用模块)exportclassTriangle{/*...*/}exportclassSquare{/*...*/}上述直接使用模块才是正确的做法。在模块系统本身,可以避免变量命名重复,所以命名空间是没有意义的。(5)限制函数参数个数定义函数时,应减少函数参数个数,建议函数参数个数不超过3个。错误用法functiongetList(searchName:string,pageNum:number,pageSize:number,key1:string,key2:string){...}不建议函数参数超过3个。当参数超过3个时,应该使用对象来聚合。正确的用法interfaceISearchParams{searchName:string;pageNum:number;pageSize:number;key1:string;key2:string;}functiongetList(params:ISearchParams){}也扩展到React项目中,useState也是const[searchKey,setSearchKey]=useState('');const[current,setCurrent]=useState(1)const[pageSize,setPageSize]=useState(10)//写法错误const[searchParams,setSearchParams]=useState({searchKey:'',current:1,pageSize:10})//正确写法(6)模块module要尽量保证没有副作用,请不要使用模块的副作用。要保证模块的使用要先导入再使用。方法错误//Test.tswindow.x=1;classTest{}lettest=newTest()//index.tsimportfrom'./test'...test中调用了上面index.ts中引入的模块。ts文件,这个方法是导入一个有副作用的模块。正确的做法应该是保证模块非导出变量的纯度,调用者在使用模块时必须先导入再调用。正确方法//test.tsclassTest{constructor(){window.x=1}}exportdefaultTest//index.tsimportTestfrom'./test'constt=newTest();(7)禁止使用!。non-nullassertionnon-nullassertion本身不安全,主观判断存在误差。从防御性编程的角度,不推荐使用非空断言。错误用法letx:string|undefinedundefined=undefinedx!.toString()使用了非空断言,所以编译的时候不会报错,但是运行的时候会报错。建议使用可选链接。的形式。(8)使用typescript的内置函数typescript的很多内置函数都可以复用一些定义。这里就不一一介绍了。常见的有Partial、Pick、Omit、Record、extends、infer等。如果需要从已有的类型派生出新的类型,使用内置函数简单方便。此外,还可以使用联合类型、交集类型和类型合并。联合类型//基本类型letx:number|stringx=1;x="1"//多重文字类型lettype:'primary'|'danger'|'warning'|'error'='primary'值得注意的是字面赋值。lettype:'primary'|'danger'|'warning'|'error'='primary'lettest='error'type=test//报错lettest='error'asconsttype=test//正确交集类型interfaceISpider{type:stringswim:()=>void}interfaceIMan{name:string;age:number;}typeISpiderISpiderMan=ISpider&IManletbob:ISpiderMan={type:"11",swim:()=>{},name:"123",age:10}类型合并最后说一下类型合并,这是一种极不推荐的方法。在业务代码中,不建议使用类型合并,会增加代码的阅读复杂度。类型合并存在于很多地方。类型可以在类、接口、命名空间等之间进行合并。以接口为例:interfaceBox{height:number;width:number;}interfaceBox{scale:number;}letbox:Box={height:5,width:6,规模:10};上面提到的同名接口Box会合并类型。不仅接口和接口可以按类型合并,类和接口、类和命名空间等也可能有同名类型合并。个人不建议在业务代码中使用类型合并。(9)ts的条件语句和类型保护的封装方式错误if(fsm.state==='fetching'&&isEmpty(listNode)){//...}state==='fetching'&&isEmpty(listNode);}if(shouldShowSpinner(fsmInstance,listNodeInstance)){//...}正确的写法,我们将条件判断的逻辑封装成一个独立的函数。这样的写法可读性更强,从函数名就可以知道我们做了什么判断。另外封装条件语句也可以和ts的自定义类型保护挂钩。让我们看一下封装条件语句的最简单的自定义类型保护。functionIsString(input:any):inputisstring{returntypeofinput==='string';}functionfoo(input:string|number){if(IsString(input)){input.toString()//判断为string}else{}}在项目中合理使用自定义守卫,可以帮助我们减少很多不必要的类型断言,提高代码的可读性。(10)不要使用未命名的变量。无论是变量名还是函数名,请不要使用非命名。我在业务中遇到过这个问题。后端定义了一个未命名的变量isNotRefresh:letisNotRefresh=false//是否不刷新,不刷新isNotRefresh表示不刷新,这样定义的变量会导致与这个变量相关的很多逻辑相反。正确的形式应该是定义变量isRefresh来表示是否刷新。letisRefresh=false//是否刷新,表示刷新2.函数式风格个人推荐函数式编程,主观上认为链式调用比回调好,函数式方式比链式调用好。近年来,函数式编程越来越流行,Ramdajs、RxJS、cycleJS、lodashJS等各种开源库都使用了函数式的特性。本文主要介绍如何使用ramdajs来简化代码。(1)声明式和命令式个人认为函数声明式的调用比命令式更简洁,例如://命令式letnames:string[]=[]for(leti=0;i
