最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Typescript代码整洁之道

    正文概述 掘金(yuxiaoliang)   2020-12-04   485

        最近半年陆续交接了几位同事的代码,发现虽然用了严格的eslint来规范代码的书写方式,同时项目也全量使用了Typescript,但是在review代码的过程中,还是有很多不整洁不规范的地方。良好的代码具有很好的可读性,后续维护起来也会令人愉悦,也能降低重构的概率。本文会结合Typescript,谈谈如何clean代码:


    一、基础规范

    (1)常量

         常量必须命名, 在做逻辑判断的时候,也不允许直接对比没有命名的常量。

    • 错误的书写
      switch(num){
           case 1:
             ...
           case 3:
             ...
           case 7:
             ...
      }
        
      if(x === 0){
           ...
      }
    
    

        上述的例子中,根本不知道1 3 7 对应的是什么意思,这种写法就基本上没有可读性。

    • 正确的写法
        enum DayEnum {
            oneDay = 1,
            threeDay = 3,
            oneWeek = 7,
        }
        let num  = 1;
        switch(num){
            case DayEnum.oneDay:
            ...
            case DayEnum.threeDay:
            ...
            case DayEnum.oneWeek:
            ...
        }
    
    
       const RightCode = 0;
       if(x === RightCode)
    

        从上述正确的写法可以看出来,常量有了命名,在switch或者if等逻辑判断的时候,我们可以从变量名得知常量的具体含义,增加了可读性。

    (2)枚举

        除了常量枚举外,在Typescript的编译阶段,枚举会生成一个maping对象,如果不是字符串枚举,甚至会生成一个双向的mapping。因此在我们的业务代码中,有了枚举,就不需要一个与枚举值相关的数组。

    • 错误的写法
    enum FruitEnum {
           tomato = 1,
           banana =  2,
           apple = 3
    }
    
    const FruitList = [
      {
         key:1,
         value: 'tomato'
      },{
         key:2,
         value: 'banana'
      },{
         key:3,
         value: 'apple'
      }
    ]
    
    

        这里错误的原因是冗余,我们要得到一个FruitList,并不需要new一个,而是可以直接根据FruitEnum的枚举来生成一个数组,原理就是我们之前所说的Typescript的枚举,除了常量枚举外,在编译的时候是会生成一个map对象的。

    • 正确的写法

    enum FruitEnum {
        tomato = 1,
        banana =  2,
        apple = 3
    }
    const FruitList = Object.entity(FruitEnum)
    

        上述就是正确的写法,这种写法不仅仅是不冗余,此外,如果修改了枚举的类型,我们只要直接修改枚举,这样衍生的数组也会改变。

        除此之外,字符串枚举值和字符串是有本质区别的,在定义类型的时候请千万注意,要不然会让你写的代码很冗余。

    • 错误的用法
    enum GenderEnum{
      'male' = '男生',
      'female' = '女生'
    }
    interface IPerson{
       name:string
       gender:string
    }
    let bob:IPerson = {name:"bob",gender:'male'}
    
    <span>{Gender[bob.gender as keyof typeof GenderEnum]}</span>  
    
    

        上述的错误的原因就是IPerson的类型定义中,gender不应该是string,而应该是一个枚举的key,因此,在将string转枚举值的时候,必须增加一个as keyof typeof GenderEnum的断言

    • 正确的写法
    enum GenderEnum{
      'male' = '男生',
      'female' = '女生'
    }
    interface IPerson{
       name:string
       gender:keyof typeof GenderEnum
    }
    let bob:IPerson = {name:"bob",gender:'male'}
    
    <span>{Gender[bob.gender]}</span>  
    
    

        上述 就是正确的写法,字符串枚举和字符串类型是有 明显区别的,当某个变量需要使用到枚举时,不能将他定义成string

    (3)ts-ignore & any

        Typescript中应该严格禁止使用ts-ignore,ts-ignore是一个比any更加影响Typescript代码质量的因素。对于any,在我的项目中曾一度想把any也禁掉,但是有一些场景中是需要使用any的,因此没有粗鲁的禁止any的使用。但是绝大部分场景下,你可能都不需要使用any.需要使用any的场景,可以case by case的分析。

    • 错误使用ts-ignore的场景
     //@ts-ignore 
     import Plugin from 'someModule' //如果someModule的声明不存在
     Plugin.test("hello world")
    
    

        上述就是最经典的使用ts-ignore的场景,如上的方式使用了ts-ignore.那么Typescript会认为Plugin的类型是any。正确的方法通过declare module的方法自定义需要使用到的类型.

    • 正确的方法
    import Plugin from 'someModule'
    declare module 'someModule' {
        export type test = (arg: string) => void;
    }
    

        在module内部可以定义声明,同名的声明遵循一定 的合并原则,如果要扩展三方模块,declare module是很方便的。

        同样的大部分场景下,你也不需要使用any,部分场景下如果无法立刻确定某个值的类型,我们可以 用unknown来代替使用any。

        any会完全失去类型判断,本身其实是比较危险的,且使用any就相当于放弃了类型检测,也就基本上放弃了typescript。举例来说:

    let fish:any = {
           type:'animal',
           swim:()=> {
           
           }
    }
    fish.run()
    
    

        上述的例子中我们调用了一个不存在的方法 ,因为使用了any,因此跳过了静态类型检测,因此是不安全的。运行时会出错,如果无法立刻确定某个值的类型,我们可以 用unknown来代替使用any。

    let fish:unknown = {
          type:'animal',
          swim:()=> {
          
          }
    }
    fish.run() //会报错
    

        unkonwn是任何类型的子类型,因此跟any一样,任意类型都可以赋值给unkonwn。与any不同的是,unkonwn的变量必须明确自己的类型,类型收缩或者类型断言后,unkonwn的变量才可以正常使用其上定义的方法和变量。

         简单来说,unkonwn需要在使用前,强制判断其类型

    (4)namespace

        Typescript的代码中,特别是偏业务的开发中,你基本上是用不到namespace的。此外module在nodejs中天然支持,此外在es6(next)中 es module也成为了一个语言级的规范,因此Typescript官方也是推荐使用module。

        namespace简单来说就是一个全局对象,当然我们也可以把namespace放在module中,但是namespace放在module中也是有问题的。

    • 错误的方法
    //在一个shapes.ts的模块中使用
    
    export namespace Shapes {
        export class Triangle {
          /* ... */
        }
        export class Square {
          /* ... */
        }
    }
    
    //我们使用shapes.ts的时候
    //shapeConsumer.ts
    
    import * as shapes from "./shapes";
    let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
    
    
    • 正确的方法(直接使用module)
    export class Triangle {
    /* ... */
    }
    export class Square {
    /* ... */
    }
    

        上述直接使用module,就是正确的方法,在模块系统中本身就可以避免变量命名重复,因此namespace是没有意义的。

    (5)限制函数参数的个数

        在定义函数的时候,应该减少函数参数的个数,推荐不能超过3个。

    • 错误的用法
    function getList(searchName:string,pageNum:number,pageSize:number,key1:string,key2:string){
       ...
    }
    
    

        不推荐函数的参数超过3个,当超过3个的时候,应该使用对象来聚合。

    • 正确的用法
    interface ISearchParams{
       searchName:string;
       pageNum:number;
       pageSize:number;
       key1:string;
       key2:string;
    }
    
    function getList(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模块尽量保证无副作用

        请不要使用模块的副作用。要保证模块的使用应该是先import再使用。

    • 错误的方法
    //Test.ts
    window.x = 1;
    class Test{
    
    }
    let test = new Test()
    
    
    //index.ts
    import from './test'
    ...
    

        上述在index.ts中import的模块,其调用是在test.ts文件内部的,这种方法就是import了一个有副作用的模块。

        正确的方法应该是保证模块非export变量的纯净,且调用方在使用模块的时候要先import,后调用。

    • 正确的方法
    //test.ts
    class Test{
       constructor(){
          window.x = 1
       }
    
    }
    export default Test
    
    //index.ts
    import Test from './test'
    const t = new Test();
    
    

    (7)禁止使用!.非空断言

        非空断言本身是不安全的,主观的判断存在误差,从防御性编程的角度,是不推荐使用非空断言的。

    • 错误的用法
    let x:string|undefined = undefined
    x!.toString()
    
    

        因为使用了非空断言,因此编译的时候不会报错,但是运行的时候会报错.

        比较推荐使用的是optional chaining。以?.的形式。

    (8)使用typescript的内置函数

        typescript的很多内置函数都可以复用一些定义。这里不会一一介绍,常见的有Partial、Pick、Omit、Record、extends、infer等等,如果需要在已有的类型上,衍生出新的类型,那么使用内置函数是简单和方便的。     此外还可以使用 联合类型、交叉类型和类型合并。

    • 联合类型
    //基本类型
    let x:number|string
    x= 1;
    x = "1"
    
    //多字面量类型 
    let type:'primary'|'danger'|'warning'|'error' =  'primary'
    
    

        值得注意的是字面量的赋值。

    let type:'primary'|'danger'|'warning'|'error' =  'primary'
    
    let test = 'error'
    type = test  //报错
    
    let test = 'error' as const 
    type =  test //正确
    
    
    • 交叉类型
    interface ISpider{
       type:string
       swim:()=>void
    }
    interface IMan{
       name:string;
       age:number;
    }
    type ISpiderMan = ISpider & IMan
    let bob:ISpiderMan  = {type:"11",swim:()=>{},name:"123",age:10}
    
    
    • 类型合并

        最后讲一讲类型合并,这是一种极其不推荐的方法。在业务代码中,不推荐使用类型合并,这样会增加代码的阅读复杂度。     类型合并存在很多地方。class、interface、namespace等之间都可以进行类型合并,以interface为例:

    interface Box {
      height: number;
      width: number;
    }
    
    interface Box {
      scale: number;
    }
    
    let box: Box = { height: 5, width: 6, scale: 10 };
    
    

        上述同名的interface Box是会发生类型合并的。不仅interface和 interface可以类型合并,class和interface,class和namesppace等等都可能存在同名类型合并,在业务代码中个人不推荐使用类型合并。

    (9)封装条件语句以及ts的类型守卫

    • 错误的写法
    if (fsm.state === 'fetching' && isEmpty(listNode)) {
     // ...
    }
    
    
    • 正确的写法
    function shouldShowSpinner(fsm, listNode) {
         return fsm.state === 'fetching' && isEmpty(listNode);
    }
    
       if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
         // ...
       }
    

        在正确的写法中我们封装了条件判断的逻辑成一个独立函数。这种写法比较可读,我们从函数名就能知道做了一个什么判断。

        此外封装条件语句也可以跟ts的自定义类型守卫挂钩。来看一个最简单的封装条件语句的自定义类型守卫。

    function IsString (input: any): input is string { 
        return typeof input === 'string';
    }
    function foo (input: string | number) {
         if (IsString(input)) {
            input.toString() //被判断为string
         } else {
         
         }
    }
    
    

    在项目中合理地使用自定义守卫,可以帮助我们减少很多不必要的类型断言,同时改善代码的可读性。

    (10)不要使用非变量

        不管是变量名还是函数名,请千万不要使用非命名,在业务中我就遇到过这个问题,后端定义了一个非命名形式的变量isNotRefresh:

    
    let isNotRefresh = false  //是否不刷新,否表示刷新
    
    

        isNotRefresh表示不刷新,这样定义的变量会导致跟这个变量相关的很多逻辑都是相反的。正确的形式应该是定义变量是isRefresh表示是否刷新。

    
    let isRefresh = false  //是否刷新,是表示刷新
    
    

    二、函数式

        个人非常推荐函数式编程,主观的认为链式调用优于回调,函数式的方式又优于链式调用。近年来,函数式编程日益流行,Ramdajs、RxJS、cycleJS、lodashJS等多种开源库都使用了函数式的特性。本文主要介绍一下如何使用ramdajs来简化代码。

    (1)声明式和命令式

        个人认为函数声明式的调用比命令式更加简洁,举例来说:

    //命令式
    let names:string[] = []
    for(let i=0;i<persons.length;i++){
            names.push(person[i].name)
    }
    
    //声明式
    let names = persons.map((item)=>item.name)
    

        从上述例子我们可以看出来,明显函数调用声明式的方法更加简洁。此外对于没有副作用的函数,比如上述的map函数,完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

    (2)Ramdajs

        推荐使用ramdajs,ramdajs是一款优秀的函数式编程库,与其他函数式编程库相比较,ramdajs是自动柯里化的,且ramdajs提供的函数从不改变用户已有数据。

        来自最近业务代码中的一个简单的例子:

       /**
        * 获取标签列表
        */
       const getList = async () => {
           pipeWithP([
               () => setLoading(true),
               async () =>
                   request.get('', {
                       params: {action: API.getList},
                   }),
               async (res: IServerRes) => {
                   R.ifElse(
                     R.isEqual(res.message === 'success'),
                     () => setList(res.response.list);
                   )();
               },
               () => setLoading(false)
           ])();
       };
    

        上述是业务代码中的一个例子,利用pipe可以使得流程的操作较为清晰,此外也不用定义中间变量。

        再来看一个例子:

    let persons = [
    
          {username: 'bob', age: 30, tags: ['work', 'boring']},
          {username: 'jim', age: 25, tags: ['home', 'fun']},
          {username: 'jane', age: 30, tags: ['vacation', 'fun']}
          
    ]
    
    

    我们需要从这个数组中找出tags包含fun的对象。如果用命令式:

    let NAME = 'fun'
    let person;
    for(let i=0;i<persons.length;i++){
       let isFind = false
       let arr = persons[i].tags;
       for(let j = 0;j<arr.length;j++){
          if(arr[i] === NAME){
             isFind = true
             break;
          }
       }
       if(isFind){
          person = person[i]
          break;
       }
    
    }
    
    

        我们用函数式的写法可以简化:

      let person = R.filter(R.where({tags: R.includes('fun')}))
    
    

    很明显减少了代码量且更加容易理解含义。

        最后再来看一个例子:

     const oldArr= [[[[[{name: 'yuxiaoliang'}]]]]];
     
    

        我们想把oldArr这个多维数组,最内层的那个name,由小写转成大写,用函数式可以直接这样写。

     R.map(atem =>
          R.map(btem => R.map(ctem => R.map(dtem => R.map(etem => etem.name.toUpperCase())(dtem))(ctem))(btem))(atem),
      )(arr);
    
    

    起源地下载网 » Typescript代码整洁之道

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元