【问题标题】:TypeScript type narrowing for callback of generic function用于泛型函数回调的 TypeScript 类型缩小
【发布时间】:2021-08-24 03:48:55
【问题描述】:

我正在尝试实现此处概述的多维数组排序功能: https://stackoverflow.com/a/55620991/2175753

它并不过分复杂,但是函数的类型设置让我头疼。

这是我目前所拥有的:

export function orderBy<T, U extends keyof T>(array: Array<T>, keys: Array<U>, sort: (key: U, a: T[U], b: T[U]) => number) {
    const records = keys.map(key => {
        const record = {
            key,
            uniques: Array.from(new Set(array.map(item => item[key])))
        }
    
        record.uniques = record.uniques.sort((a, b) => sort(key, a, b))
    
        return record
    })
    
    const weightOfObject = (obj: T) => {
        let weight = ''
        records.map(record => {
            let zeropad = `${record.uniques.length}`.length
            weight += record.uniques.indexOf(obj[record.key]).toString().padStart(zeropad, '0')
        })
    
        return weight
    }

    return array.sort((a, b) => weightOfObject(a).localeCompare(weightOfObject(b)))
}

在调用站点上,它看起来像这样:

问题是,我无法进一步缩小 a 和 b 的类型。 我希望这会起作用:

我尝试调整函数签名以明确表示回调是通用的,如下所示:

export function orderBy<T, U extends keyof T>(array: Array<T>, keys: Array<U>, sort: <V extends U>(key: V, a: T[V], b: T[V]) => number) { ... }

但无济于事。

这是 TypeScript 不支持的东西还是我做错了什么?

【问题讨论】:

    标签: typescript typescript-typings typescript-generics


    【解决方案1】:

    问题是打字稿只跟踪值类型之间的连接,只要它们在同一个变量中,比如一个对象的两个属性或一个元组的两个元素。一旦您将这些值分成不同的变量,例如通过解构它们或仅在函数中作为 2 个不同的参数传递,就像您的情况一样,所有连接都会丢失并且无法恢复它们(至少现在,也许有一天它会成为可能)。考虑一个可能经常出现的情况

    type Foo = 
      | { type: 'a', value: number }
      | { type: 'b', value: string }
    
    declare const obj: Foo
    // Here there is connection between obj.type and obj.value
    switch(obj.type) {
      case 'a':
        // Here obj.value is of type number
        break
      case 'b':
        // Here obj.value is of type string
        break
    }
    

    但如果你这样做

    const {type, value} = obj
    // At this point type and value are distinct variables 
    // and any connection they had is lost
    switch(type) {
      case 'a':
        // Here value is number | string
        break
      case 'b':
        // And here as well
        break
    }
    

    您对此无能为力,您必须将值保留在同一个对象中,以便打字稿保留有关它们的类型如何相互依赖的任何有用信息。

    所以这正是您在您的场景中可以做的:将这些值放入一个对象中并且不要对其进行解构。这是一种方法。第一次尝试可能如下所示:

    declare function orderBy<T, U extends keyof T>(
      array: T[], 
      keys: U[], 
      sort: (data: {key: U, a: T[U], b: T[U]}) => number
    ): T[]
    

    但它实际上不起作用,因为在您提供的特定示例中T[U]number | Date,因此类型之间的依赖关系立即丢失。要保留它,您需要“映射”此联合类型U,以便此联合的每个成员都映射到具有合适类型的对象。这可以通过这样的辅助类型来完成:

    type MapKeysToSortData<T, U extends keyof T> = {
      [TKey in U]: {
        key: TKey,
        a: T[TKey],
        b: T[TKey],
      }
    }[U]
    

    所以你正在创建一个对象,其中键是U 的成员,你为每个严格类型的键分配一个对象,然后你得到这些对象的联合,最后是这个[U]。所以函数看起来像这样:

    declare function orderBy<T, U extends keyof T>(
      array: T[], 
      keys: U[], 
      sort: (data: MapKeysToSortData<T, U>) => number
    ): T[]
    

    差不多就是这样。在向此声明添加实现时,您可能会遇到一些问题,我不打算在这里输入它,无论如何您可以输入一些类型断言,它会做到的。虽然我很确定你可以在 2021 年在没有类型断言的情况下输入它。

    现在使用函数不解构回调中的参数(至少直​​到打字稿已经推断出你需要的一切)

    orderBy([testObject], ['age', 'eta'], (data) => {
      if(data.key === 'eta') {
        // Here TS already knows data.a and data.b are of type Date, so you can destructure them
        const {a, b} = data
        console.log(a.toUTCString(), b.toUTCString())
      } else {
        // Here data.a and data.b are of type number
      }
      return 1
    })
    

    Check out a sandbox

    我还要注意,你不是在排序多维数组,只是对对象的一维数组进行排序。并且请不要插入代码的截图,将实际代码复制为文本,并添加描述 IDE 的这些悬停事物的 cmets,从图像中复制测试数据有点烦人。

    【讨论】:

    • 哇!很详细的回答,谢谢。您的解决方案有效,尽管我不得不承认我还没有完全理解它。我想我需要大量阅读类型系统。 :) 一件事:当我启用严格模式时,编译器会警告我数据可能未定义。我可以通过可选链接(data?.key)来解决它。您知道为什么会出现警告吗?
    • 您共享的沙箱与我在本地使用的 tsconfig 相同。沙箱不会警告数据可能未定义,VSCode 会。很奇怪。
    • 好吧,不,老实说,我不明白为什么会有这样的警告,因为没有未定义的字段:p 也许 vscode 有点困惑,你需要重新启动它,也许你的代码和沙盒有一些区别,我不能说
    猜你喜欢
    • 1970-01-01
    • 2020-06-09
    • 1970-01-01
    • 2018-07-03
    • 1970-01-01
    • 1970-01-01
    • 2021-10-06
    • 1970-01-01
    • 2023-03-30
    相关资源
    最近更新 更多