泛型函数回调的TypeScript类型收缩

8tntrjer  于 2023-06-30  发布在  TypeScript
关注(0)|答案(1)|浏览(144)

我正在尝试实现一个多维数组排序函数,如下所示: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)))
}

在callsite上,它看起来像这样:

问题是,我无法进一步缩小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不支持的,还是我做错了什么?

hmtdttj4

hmtdttj41#

UPD:这个答案部分已经过时了,因为TS 4.x的后期版本引入了去结构化变量之间的跟踪关系。然而,函数参数之间的关系可能永远不会被保留(除了重载函数,但它们在OP的情况下没有用),因此建议的解决方案仍然有效。

某个过时的部件

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

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
}

你不能做任何事情,你必须把这些值保存在同一个对象中,以便typescript保留关于它们的类型如何相互依赖的任何有用信息。

有效溶液

这就是你在你的场景中可以做的:把这些值放到一个对象中,而不破坏它。这里有一个方法。第一次尝试可能看起来像这样:

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,因此类型之间的依赖性立即丢失。要保留它,您需要“Map”这个联合类型U,以便这个联合的每个成员都Map到具有拟合类型的对象。这可以通过像这样的helper类型来实现:

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[]

差不多就是这样。你可能会遇到一些问题,当添加实现到这个声明,我不打算在这里输入它,无论如何,你可以抛出一些类型Assert,它会这样做。尽管我很确定你可以在2021年不使用类型Assert来输入它。
现在使用函数 * 而不破坏 * 回调中的参数(至少在typescript已经推断出您需要的所有内容之前)

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中这些悬停的注解,从图像中复制测试数据有点烦人。

相关问题