打字稿通用约束查找由两种类型共享的属性名称

时间:2020-08-21 16:11:34

标签: javascript typescript generics

我有一个通用函数,如下所示:

function joinBy<T, U, K>(data1: Array<T>, data2: Array<U>, key: K) {
  return data2.map( datum2 => {
    return data1.find( datum1 => datum1[key] === datum2[key] )
  })
}

我想做的是将K约束为TU都具有的属性的字符串。例如,给定以下类型:

type Customer = {
  customerId: number,
  name: string,
  email?: string
}

type Order = {
  customerId: number,
  orderId: number,
  items: Array<Item>
}

K的唯一有效值应该是customerId,因为这两种类型都通用。如果两种类型都有另一个共同的字段,例如foo,则K应该是联合'customerId' | 'foo'

如果我将函数签名修改为以下内容:

function joinBy<T, U, K extends keyof T & keyof U>(data1: Array<T>, data2: Array<U>, key: K) {
  return data2.map( datum2 => {
    return data1.find( datum1 => datum1[key] === datum2[key] )
  })
}

datum1[key] === datum2[key]上的打字稿错误,表示为types T[K] and U[K] have no overlap。从this Github issue中,我知道有时该错误可能是误报,但是我不确定这是否是其中之一,或者是否有更好的方法来构建K的类型约束。

是否有更好的方法可以做到这一点?或者我只是看到误报?谢谢!

1 个答案:

答案 0 :(得分:1)

为了使操作安全,您不仅需要确保Kkeyof T中都包含keyof U,还需要确保T[K]和{ {1}}是同一类型(实际上,它们可以通过U[K]进行比较)。否则,您可能会发现自己接受这种事情:

===

interface Tree { name: string; age: number; bark: string; } interface Dog { name: string; age: number; bark(): void; } declare const trees: Tree[]; declare const dogs: Dog[]; joinBy(trees, dogs, "bark"); Tree都有一个名为Dog的属性,但它们不可比。

有多种方法可以加强签名,以要求barkT[K]兼容……实际上是多种方法。

TS的一个主要警告是,在某些情况下,尤其是在{{1}的实现中使用未指定的泛型类型参数(例如U[K]TU时) }),人们可以看到某些东西是类型安全的,但是编译器却不能,因为编译器没有对通用类型执行正确的“高阶”推理。

因此,为此类函数提供类型签名的技术的一部分不仅是获得正确的约束,而且是编译器可以实际验证函数实现内部的类型安全性的约束。这并非总是可能的,因此有时您必须在实现内诉诸type assertions之类。

不过,对于您的功能,有一个“易于编译的”类型:

K

在这里,编译器只需要关心joinBy()数组的元素类型function joinBy<T, K extends keyof T>(data1: T[], data2: (Pick<T, K>)[], key: K) { return data2.map(datum2 => { return data1.find(datum1 => datum1[key] === datum2[key]) }) } ,并且T的类型data1是其已知键之一。然后,对K的约束是它必须是可分配给key的某种类型的数组:即,具有data2属性的类型与{{1 }}。我们实际上并不在乎Pick<T, K>的具体类型是什么,因为我们只关注其K属性。我们不会返回该元素类型的任何值,因此我们无需花费任何精力为其推断类型参数T

并且编译器很乐意允许data2,因为它会将两者都视为类型key(或U,很幸运,它们是兼容的)。

让我们确保它能起作用:

datum1[key] === datum2[key]

那太好了。


但是,正如您在评论中提到的那样,感觉电话签名上的错误在错误的位置。您可能希望看到T[K]和/或Pick<T, K>[K]是错误的来源,而不是declare const customers: Customer[]; declare const orders: Order[]; joinBy(customers, orders, "customerId"); // ok joinBy(customers, orders, "name"); // error! // -------------> ~~~~~~ // 'name' is missing in Order joinBy(trees, dogs, "age"); // ok joinBy(trees, dogs, "bark"); // error! // ---------> ~~~~ // 'bark' property incompatible "name"。这是可以实现的,但是(可能)不是编译器可以在实现内部理解的方式。为了实现这一点,我将使用与类型断言等效的东西:单个调用签名overload:让调用签名对调用者来说是一个好的选择,而实现签名对实现来说是一个好的选择:

"bark"

这里的实现签名故意充满orders;您可以根据需要使用dogstype CompatibleKeys<T, U> = { [K in keyof T & keyof U]: U[K] extends T[K] ? K : T[K] extends U[K] ? K : never }[keyof T & keyof U]; function joinBy<T, U, K extends CompatibleKeys<T, U>>( data1: T[], data2: U[], key: K ): (T | undefined)[]; function joinBy(data1: any[], data2: any[], key: PropertyKey) { return data2.map(datum2 => { return data1.find(datum1 => datum1[key] === datum2[key]) }) } 保留原始版本,但是从某种意义上说,一旦您愿意将调用方与实现方分开,就没关系了:随心所欲在实现中,请确保您个人确信其类型安全性,然后为呼叫方提供一个符合您所需行为的签名。

让我们看看它现在的表现:

any

现在错误出现在T参数上,它们或多或少告诉您问题出在哪里。是的。

虽然不确定KjoinBy(customers, orders, "name"); // error! // ---------------------> ~~~~~~ // Argument of type '"name"' is not assignable to parameter of type '"customerId"'. joinBy(trees, dogs, "bark"); // error! // ---------------> ~~~~~~ // Argument of type '"bark"' is not assignable to parameter of type 'CompatibleKeys<Tree, Dog>' 之间实际上没有重叠的情况,但是您不确定要怎么做。以下应该是一个错误,但是什么错误?

key

我真的觉得T是这里的错误,因为您没有可写的有效U。为了使 成为现实,您可以开始为joinBy(orders, trees, "name"); // error! // -----------------> ~~~~~~ // Argument of type 'string' is not assignable to parameter of type 'never'. 签名添加更多的复杂性……也许像这样:

trees

现在,除非没有重叠,否则key上会出现错误,在这种情况下,joinBy()上会出现错误:

type SomeCompatibleType<T> = { [K in keyof T]: Pick<T, K> }[keyof T]

function joinBy<T, U extends SomeCompatibleType<T>, K extends CompatibleKeys<T, U>>(
  data1: T[],
  data2: U[],
  key: K
): (T | undefined)[];
// impl elided

再次。也许不是……到目前为止,代码是如此复杂,以至于我不愿意将其放在重要位置。可能会有一些奇怪的情况,很多人将看不到它在做什么。因此,备份时,我建议您使用真正对编译器友好的签名。


好的,希望能有所帮助;祝你好运!

Playground link