IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    TypeScript 泛型解析

    usubeni fanstay发表于 2023-10-17 17:15:42
    love 0

    泛型入门

    泛型简单来说可以理解成把类型当变量传到类型定义里,就如同参数传到函数一样,例如:

    function identity<Type>(arg: Type): Type {
      return arg
    }
    
    let output = identity<string>('myString')

    使用 <> 包裹 Type 就能把类型传入泛型函数,上面函数的效果是:接受类型为 Type 的参数,返回类型为 Type 的结果。

    既然 Type 是个参数,那名字自然也是很自由的,我们常常会使用 T 当参数的名称,所以可以这么写:

    function identity<T>(arg: T): T {
      return arg
    }

    自我推断

    在使用泛型函数时可以不明确指出 T 的类型(而且我们通常都会这么做),此时 TS 将自动推断 T 的类型:

    let output = identity('myString')
    // output 也是 string

    还是上面的例子,如果不显式指定类型 <string>,TS 就直接推断 "myString" 的类型为 T,所以这样函数返回的也是字符串。

    画个圈

    默认状态下泛型可以是任何类型,这样可读性就很低了,而且在对“施加了泛型”的类型进行操作或调用起方法时,因为是任意类型,必然不能通过检查,为了解决这个问题,可以通过 extends 给泛型画个圈,框定它的类型。

    interface Lengthwise {
      length: number
    }
    
    function loggingIdentity<T extends Lengthwise>(arg: T): T {
      console.log(arg.length) // arg 必定有 length 属性,通过类型检查
      return arg
    }

    上面的代码通过 <T extends Lengthwise> 表明这个 T 必须是一个有 length 属性的类型,任何有 length 属性的类型都满足这个泛型的需求,例如,它也可以是一个数组。

    绑定能力

    据官网所说泛型用于复用类型,相信经过上面的简单介绍也会觉得这确实十分有效。但是泛型除了用于类型复用,还有什么其他运用呢?

    我的答案是类型的联动,T 可以对同一个类型定义内运用到的其他泛型进行绑定。

    再看一眼这个例子,其实他就是把输入的类型和输出的类型进行了绑定:

    function identity<Type>(arg: Type): Type {
      return arg
    }

    下面看一个“类型绑定”玩法更显眼的例子。

    function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
      return obj[key]
    }
    
    let x = { a: 1, b: 2, c: 3, d: 4 }
    
    getProperty(x, 'a') // 可以
    getProperty(x, 'm') // 报错,因为 `Key` 绑定为 `Type` 的 key,而 m 并非 `Type` 的 key

    映射类型

    const myMap = {
      a: () => {},
      b: (someString: string) => {},
      c: (someNumber: number) => {},
    }
    type MyMap = typeof myMap
    type MyKey = keyof MyMap

    假设有一个对象,它的 key 是 a、b、c,值是不同函数,现在我们需要得到一个 key 和对应函数参数的对象的类型,该如何实现呢?

    type Answer = Record<MyKey, Parameters<MyMap[MyKey]>>

    如果这么写就坏了,Answer 只是一个 key 为 MyKey,value 为 Parameters<MyMap[MyKey]> 的对象,但是这两者间丢失了 myMap 定义的关系,变成这样:

    type Answer = {
      a: [] | [someString: string] | [someNumber: number]
      b: [] | [someString: string] | [someNumber: number]
      c: [] | [someString: string] | [someNumber: number]
    }

    所以这时候其实就要用到泛型对类型的绑定能力啦!正确答案如下:

    type Answer2 = {
      [K in MyKey]: Parameters<MyMap[K]>
    }

    K 是类型 myMap 的 key,并且,Answer2 的值必须是 MyMap[K] 的参数。这样就绑定了 Key 和值的固定关系。

    甚至在新版本还有这种花里胡哨的,你可以把属性类型再 as 一次:

    type Getters<Type> = {
      [Property in keyof Type as `get${Capitalize<
        string & Property
      >}`]: () => Type[Property]
    }
    
    interface Person {
      name: string
      age: number
      location: string
    }
    
    type LazyPerson = Getters<Person>

    输出结果如下:

    type LazyPerson = {
      getName: () => string
      getAge: () => number
      getLocation: () => string
    }

    P.S. as 其实是什么?官方文档称为 Type Assertions,用于把一个类型 as 为另一个类型,但是这两个类型,可以往小里 as,也可以往大去 as,但是必须有一方是另一方的子集。在 LazyPerson 的例子中因为说到底全是 string,所以可以使用 as。

    实战

    下面先放出题目,有兴趣可以先自己思考一下,如何完善下面 JS 函数的类型?(答案在下面,先别翻下去哦)

    题目

    首先我们有一个 myMap,但不直接使用它,而是先通过 wrapper 把它包装一下,这样就可以实现在运行函数时先做某些前置操作了,那么问题是,wrappedMap 的类型怎么写呢?

    const myMap = {
      a: () => {},
      b: (someString) => {},
      c: (someNumber) => {},
    }
    
    function wrapper(_key, fn) {
      return async function (...arg) {
        // do something
        await Promise.resolve()
        fn.apply(null, arg)
      }
    }
    
    const wrappedMap = {}
    for (const key in myMap) {
      const k = key
      wrappedMap[k] = wrapper(k, myMap[k])
    }

    答案

    const myMap = {
      a: () => {},
      b: (someString: string) => {},
      c: (someNumber: number) => {},
    }
    type MyMap = typeof myMap
    type MyKey = keyof MyMap
    
    function wrapper<K extends MyKey, T extends MyMap[K]>(_key: K, fn: T) {
      return async function (...arg: Parameters<T>) {
        await Promise.resolve()
        ;(fn as any).apply(null, arg)
      }
    }
    
    type WrappedMap = {
      [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
    }
    
    const wrappedMap: Partial<WrappedMap> = {}
    for (const key in myMap) {
      const k = key as MyKey
      wrappedMap[k] = wrapper(k, myMap[k])
    }

    现在确实是已经做到了 WrappedMap 的类型是 wrapper 返回值的效果,但是,这句 (fn as any).apply(null, arg),是不是显得很突兀?

    为什么还需要把 fn 置为 any?

    因为对 TS 来说 a、b、c 根本没有和他的值的参数类型进行绑定,所以即使用了 T 进行限制也没有效果,这句话可能有点拗口,接着看下面的答案 2 可能会更清晰。

    答案 2

    const myMap: MyMap = {
      a: () => {},
      b: (someString: string) => {},
      c: (someNumber: number) => {},
    }
    interface MyMapArgs {
      a: []
      b: [someString: string]
      c: [someNumber: number]
    }
    
    type MyMap = {
      [K in keyof MyMapArgs]: (...args: MyMapArgs[K]) => void
    }
    
    type MyKey = keyof MyMap
    
    function wrapper<K extends MyKey, F extends MyMap[K]>(_key: K, fn: F) {
      return async function (...arg: Parameters<F>) {
        await Promise.resolve()
        fn.apply(null, arg)
      }
    }
    
    type WrappedMay = {
      [K in MyKey]: ReturnType<typeof wrapper<K, MyMap[K]>>
    }
    
    const wrappedMap: Partial<WrappedMay> = {}
    for (const key in myMap) {
      const k = key as MyKey
      wrappedMap[k] = wrapper(k, myMap[k])
    }

    去除 (fn as any) 的解法是,先另外造一个 map 把你需要关联的东西先映射一遍,就是上面的 MyMapArgs,接着再用这个映射造出 MyMap,这样 TS 才终于明白这两个东西是有关系的。

    P.S. 更详细的信息可以参考 issues#30581 和 pull#47109



沪ICP备19023445号-2号
友情链接