最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 将小驼峰接口类型递归的转成大驼峰:TypeScript 高级类型与 4.1 字符串模板类型实战

    正文概述 掘金(wwwzbwcom)   2021-03-01   1118

    在 Javascript 中,变量名,键的命名风格一般都是 camelCase 的,而在许多接口规范中,则都是接受 PascalCase 风格的 JSON Object。完成大驼峰参数到小驼峰参数的转换,这在 Javascript 中是较容易实现的。可是在 Typescript 中,我们进行了这样的转换后,就会丢失类型的信息。这导致我们每次进行转换后,都必须重写类型,或是放弃后续的类型检查。

    • 忽略后续类型违反了重构 Typescript 的初衷 这导致后续的代码都失去了类型检查的保护,在后续业务变更新增后续代码,更会导致更多的问题

    • 重写类型导致需要进行小驼峰类型 <-> 大驼峰类型的重复编写工作 在编程中,可怕的不仅仅是重复工作导致的工作量增加,更严重的是,重复可能会带来错误:某个重复中的错误会导致错误难以排查,而多个重复中共有的错误,则会导致错误难以修正 这也就是违反了编程中我们常说的 DRY 原则 (Don't Repeat Yourself)

    遗憾的是,这个问题在 Typescript 4.1 以前,是简单的没有解决方法的,不过幸运的是,4.1 版本已经进入了 stable 阶段,它带来的字符串模版类型 template literal type 可以让我们修改字符串常量的类型,进行各种强大的操作,例如,通过下面的 PascalCasedPros<T>,我们就可以将小驼峰的接口类型中的每个键转为大驼峰:

    type PascalCase<T> = T extends string
      ? T extends `${infer A}${infer B}`
        ? `${Uppercase<A>}${B}`
        : T
      : T;
    
    type PascalCasedProps<T> = T extends Function
      ? T
      : T extends Array<infer U>
      ? Array<PascalCasedProps<U>>
      : {
          [K in keyof T as PascalCase<K>]: PascalCasedProps<T[K]>;
        };
    

    咋一看其中内容是非常复杂且混乱的,但让我们以具体的代码为例子,具体的一步步来解析一下:

    PascalCase<T> 将字符串常量类型 T 转换为对应的大驼峰版本

    首先,我们需要一个能将一个字符串字面量 (string literal) 类型转换为对应的大驼峰字符串字面量类型的泛型类型别名 generic type alias

    而我们要的泛型类型别名,就是如下的 PascalCase<T>:可以看到,我们传入一个字符串字面量类型,它就可以完成我们需要的转换。

    type PascalCase<T> = T extends string
      ? T extends `${infer A}${infer B}`
        ? `${Uppercase<A>}${B}`
        : T
      : T;
    
    type CheckMe = PascalCase<"HelloWorld">;
    // type CheckMe = "helloWorld"
    

    但它是如何工作的呢?让我们来逐步解析一下:

    最外部是 T extends string ? (...) : T 这样的结构,这有点像三目运算符,实际上,它起的正是这样的作用:

    • 首先, A extends B 是判断 A 类型是否扩展了 B 类型(比如一个类实现了另一个接口),在这里,T extends string 也就是判断 T 类型是否是 string
    • 如果不是 string,我们进入 : T 部分,很简单,即不做转换,直接返回 T 类型

    中间的部分则要复杂一些,涉及到字符串模板类型,让我们下面来看一下:

    T extends `${infer A}${infer B}`
        ? `${Uppercase<A>}${B}`
        : T
    
    • 首先,这部分和之前一样,也是判断 T 是否扩展了 ${infer A}${infer B} 类型

      T extends `${infer A}${infer B}`
      
    • 这里我们需要讲解一下 infer 在 typescript 中的作用,它代表让 TypeScript 的类型系统推测一个类型,也就是说,让编译器自动根据字符串字面量 T 的结构,自动推出 AB 对应的部分。

    • 理解了上面的内容,我们回过头来看完整的中间部分,就没有那么难以理解了

      T extends `${infer A}${infer B}` ? `${Uppercase<A>}${B}` : T
      

      可以看到,在编辑器工作时,将 A 推断为第一个字符,B 推断为后续部分。(当不影响后续匹配时,每个推断类型会匹配尽量少且不为空串的子字符串,所以 A 只匹配第一个字符)。例如,对于字符串 "helloWorld" 的匹配,A 将被推断为 "h", B 将被推断为后面的 "elloWorld"

      推断完成后,我们用 ${Uppercase<A>}${<B>} 将首字母小写的源串拼接回来,即完成了字符串字面量类型首字母大写的转换。

    PascalCasedProps<T> 递归的将接口 T 中的所有键转换为大驼峰

    type PascalCasedProps<T> = T extends Function
      ? T
      : T extends Array<infer U>
      ? Array<PascalCasedProps<U>>
      : {
          [K in keyof T as PascalCase<K>]: PascalCasedProps<T[K]>;
        };
    

    最后,通过这个 PascalCasedProps 的泛型类型别名,我们就可以实现我们的目的了。但是,它看起来还要更复杂,但其实基本原理还都是一样的,我们还是具体的来解析一下吧:

    • 这部分,是判断 T 是否是一个函数,若是,我们需要原样返回类型 T,否则继续进行判断:

      T extends Function : T
      
    • 这部分,则是判断我们传入的 T 类型是否是数组类型

      T extends Array<infer U>
      
      • 如果是的话,我们不能对它直接应用 PascalCasedProps<T>,(为什么不能,我们稍后进行解释)。
        • 我们要对数组项的类型做转换,也就是编译器帮助我们推导出来的 U 类型,最终的写法也就是
        T extends Array<infer U> ? Array<PascalCasedProps<U>>
        
    • 而如果 T 类型不是数组,比如是一个对象,我们要怎么处理呢?具体的,在这部分:

      {
        [K in keyof T as PascalCase<K>]: PascalCasedProps<T[K]>;
      };
      
      • 首先,在左边的 [K in keyof T as PascalCase<K>] 中,keyof T 即是指类型 T 的所有键。举一个例子:

        // type keys = 'keyA' | 'keyB'
        type keys = keyof {
          keyA: string;
          keyB: number;
        };
        

        K in keyof T 是将一个接口的键,对应到另一个接口上的写法。K 一一对应的代表了 T 中每一个键的类型。

      • 后面的 as PascalCase<K> 则是对每个键的数值映射到对应的小驼峰风格,我们则通过 T[K] 取出每个键对应的值的类型,并将它递归的转为小驼峰。至此,我们的转换就顺利完成咯。

    补充内容:为什么对数组和函数特殊处理?

    不过,为什么前面需要对数组和函数进行特判处理,而其他值不用呢?可以首先试一下,为什么要对数组进行判断:

    type PascalCase<T> = T extends string
      ? T extends `${infer A}${infer B}`
        ? `${Uppercase<A>}${B}`
        : T
      : T;
    
    type PascalCasedProps<T> = T extends Function
      ? T
      : // : T extends Array<infer U>
        // ? Array<PascalCasedProps<U>>
        {
          [K in keyof T as PascalCase<K>]: PascalCasedProps<T[K]>;
        };
    
    type A = PascalCasedProps<string[]>;
    

    如果注释掉判断数组的部分,我们转换后的类型 A 在编辑器中可以看到,变成了:

    type A = {
        [x: number]: string;
        Length: number;
        ToString: () => string;
        ToLocaleString: () => string;
        Pop: () => string | undefined;
        Push: (...items: string[]) => number;
        Concat: {
            (...items: ConcatArray<string>[]): string[];
            (...items: (string | ConcatArray<...>)[]): string[];
        };
        ... 15 more ...;
        ReduceRight: {
            ...;
        };
    }·
    

    这是因为 JavasScript 中数组也是一个对象,它的全部内置方法(例如 .Push())也被转换为大驼峰类型,而这显然不是我们想要的。

    • 而函数在转换后,则变成了一个空 Object {}:这应当是函数本身也被认为是对象,且没有任何属性的缘故吧:
    // type A = {}
    type A = PascalCasedProps<() => {}>;
    

    为什么会这样呢?我做了一个小的试验:

    let foo = 1;
    foo.bar = 2;
    console.log(foo); // 1
    console.log(foo.bar); // undefined
    
    let bar = () => {};
    bar.foo = 3;
    console.log(bar); // [Function: bar]
    console.log(bar.foo); // 3
    

    可以看到,JavaScript 中的函数确实能作为一个对象,为它的属性赋值,而非引用类型则不可以(虽然不会报错,但值并不会保存下来)

    而对于其他任何非引用类型(如 Number, String 等),经过我的试验,Typescript 由于不认为他们是对象,会直接原样返回他们的类型。

    总结

    在利用 TypeScript 进行接口风格转换的过程中,我可以说第一次认识到了 TypeScript 4.1 类型系统的强大之处。在 4.1 版本刚刚发布时,网上有许多声音认为 TypeScript 已经越来越复杂,变得和 Java,C++ 一样了,我个人看着 Template Literal Type 也是摸不着头脑,一时并没有想到这个功能的 use case。但没想到,在一次项目的 TypeScript 重构中,我就体验到了这个功能不仅强大,更及具意义。

    程序员间有一个经典的说法:程序编写时永恒的两个问题,分别是变量命名和缓存一致性。如今,随着业务变得越来越复杂,我们调用的,来自不同服务的接口的命名规范,也变得越来越多样。而作为前端工程师,不论是编写页面还是所谓的 BFF 层,如何高效的完成各类 API 的调用和聚合,在我们的开发工作中也是至关重要的。

    TypeScript 中可以说实现了应该是目前编程语言中最为强大的类型系统,掌握好其中的高级特性,我们就可以完成对许多类型的管理和转换,高效地写出严谨对应业务数据形式的代码,提高代码的可知性,尽可能在编译时避免绝大部分的问题。


    起源地下载网 » 将小驼峰接口类型递归的转成大驼峰:TypeScript 高级类型与 4.1 字符串模板类型实战

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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