首页 > 解决方案 > 用于泛型函数回调的 TypeScript 类型缩小

问题描述

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

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

这是我到目前为止所拥有的:

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 不支持的东西还是我做错了什么?

标签: typescripttypescript-typingstypescript-generics

解决方案


问题是打字稿只跟踪值类型之间的连接,只要它们在同一个变量中,比如一个对象的两个属性或一个元组的两个元素。一旦您将这些值分成不同的变量,例如通过解构它们或仅在函数中作为 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,您为每个严格键入的键分配一个对象,然后在最后得到这些对象与 this 的联合[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
})

查看沙箱

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


推荐阅读