TypeScript中工具类型的使用与源码理解(一):Extract、Exclude、Record

476 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

  1. Extract
    • 源码
      type Extract<T, U> = T extends U ? T : never
      
      通过源码,咱们很明显就能看出Extarct是来判断T类型是否为U类型的‘子类型’或者‘子集’,如果是则返回最终类型T,不是则返回最终类型never。下面通过示例了解Extarct的用法以及更加深入了解extends泛型约束。
    • 示例
      • 父子类使用Extarct,这里仅仅展示了接口的情况,类、类型也是一样,这里就不再赘述了。
        interface People {
          name: string
        }
        
        // 情况1
        interface Men extends People {
          age: number
        }
        
        // 情况2
        interface Men {
          name: string
          age: number
        }
        
        // 类型为Men, 情况一二结果一致,这就是上述的子类型或子集
        type obj = Extract<Men, People>
        
        // 类型为nerve
        type Example = Extract<string, People>
        
      • 联合类型
        // string
        type unionExtract1 = Extract<string, string | number | symbol>
        
        // string
        type unionExtract2 = Extract<string | number, string>
        
        上面例子,一看是看unionExtract1string不少人会感觉很如我所料。但是unionExtract2string就懵了,这是为啥? 这是TypeScript的检测机制,对于联合类型采用的是一一比对的策略。unionExtract2的生成过程如下: 第一次:拿出前string与后string进行Extract,返回的类型为string; 第二次:拿出前number与后string进行Extract,返回的类型为never; 将两次生成的类型进行联合就返回了结果string | never,never表示什么都没有,但是这里返回的类型可能为string,所以never的存在不成立,因而最终结果为string
      • 函数
        type func1 = (parame1: 'zhangsan', parame2: number) => string;
        type func2 = (parame1: string) => 'zhangsan';
        
        
        // never
        type funcExtract1 = Extract<func1, func2>;
        
        //  (parame1: string) => 'zhangsan'
        type funcExtract2 = Extract<func2, func1>
        
        对于函数类型来说,判断是否是子类型,有三个方面
        1. 返回值类型,子类型的返回值类型必须为父类型的返回值类型的‘子集’
        2. 参数类型,子类型的参数个数不多于父类型的参数个数。
        3. 对应位置的参数类型,父类型的对应位置的参数类型必须为子类型的对应位置的参数类型‘子集’;
  2. Exclude
    • 源码
      type Exclude<T, U> = T extends U ? never : T
      
      通过源码,咱们很明显就能看出ExcludeExtract正好相反
    • 示例
       interface Student {
         name: string,
         age: number,
       }
      
       interface Teacher {
         name: string,
         age: number,
         salary: number
       }
      
       // 获取Teacher接口中的特有key
       // "salary"
       type TeacherPeculiarType = Exclude<keyof Teacher, keyof Student>
      
  3. Record
    • 源码
      type Record<K extends keyof any, T> = {
         [P in K]: T;
      };
      
      源码中有挺多陌生的点,下面就分点解释:
    • K extends keyof T,可以判断对象类型或接口T中是否有K属性,如下
      interface Student {
        name: string,
        age: number,
      }
      
      // T[k]就如同获取对象属性一样,不过这里获取的是接口T中K属性的类型
      type OneType<T, K> = K extends keyof T ? T[K] : never;
      
      // string
      type StudentOne = OneType<Student, 'name'>
      
      K extends keyof T这个的用法懂了,那么源码中的K extends keyof any又是啥东西呢? TypeScript中规定keyof any返回联合类型string | number | symbol K extends keyof any相当于K extends keyof string | number | symbol
      Record中起到约束泛型,确保是有效的key
      // string | number | symbol
      type AnyKeyType = = keyof any
      
    • P in K,这里的inJs中的forin有一丢丢相似,都有迭代的意思,具体看示例: in后面的类型范围必须在string | number | symbol之内。
      • in后面的类型范围为具体字符串时,会根据所有的字符串迭代,并作为key
        type InTestType = {
          [p in 'age']: string
        }
        //上述类型相当于
        type InTestType = {
            age: string;
        }
        
        type InTestType2 = {
          [p in 'age' | 'name']: string
        }
        //上述类型相当于
        type InTestType = {
            age: string;
            name: string
        }      
        
      • in后面的类型为string, 相当于只要键名为字符串就可以,对具体键名无要求
        type InTestType = {
          [p in string]: string
        }
        
        // 相当于
        type InTestType = {
            [x: string]: string;
        }
        
      • in后面的类型为string | number | symbol,这三个类型单个/成双/成三联合时
        type InTestType = {
          [p in symbol | string]: string;
        }
        
        // 相当于
        type InTestType = {
            [x: string]: string;
            [x: symbol]: string;
        }
        
      • in后面的类型为number, 相当于只要键名为数字就可以,对具体键名无要求。可以表示数组类型
        type InTestType = {
          [p in number]: string
        }
        
        // 相当于
        type InTestType = {
            [x: number]: string;
        }
        
        // 正确
        let inTest: InTestType = {
          0.1: '123'
        }
        
        // 正确
        let inArrayTest: InTestType = ['fsdf', 'dsfsdf', 'sdfdsf']
        
    • 示例
      interface ProductType {
        id: number,
        [x: string]: any
      }
      
      function arrayToObj<T extends ProductType>(arr: T[]): Record<number, T> {
        const res: Record<number, T> = {};
        arr.forEach(item => {
          res[item.id] = item
        })
        return res;
      }
      
      interface GoodsType {
        id: number,
        name: string,
        price: number
      }
      
      const goods: GoodsType[] = [
        {
          id: 102,
          name: 'dfas',
          price: 13
        },
        {
          id: 108,
          name: 'dsa',
          price: 15
        }
      ]
      
      // 输出
      // {
      //   '102': { id: 102, name: 'dfas', price: 13 },
      //   '108': { id: 108, name: 'dsa', price: 15 }
      // }
      console.log(arrayToObj<GoodsType>(goods));