我有一个通用函数,如下所示:
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
约束为T
和U
都具有的属性的字符串。例如,给定以下类型:
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
的类型约束。>
是否有更好的方法可以做到这一点?或者我只是看到误报?谢谢!
答案 0 :(得分:1)
为了使操作安全,您不仅需要确保K
和keyof 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
的属性,但它们不可比。
有多种方法可以加强签名,以要求bark
和T[K]
兼容……实际上是多种方法。
TS的一个主要警告是,在某些情况下,尤其是在{{1}的实现中使用未指定的泛型类型参数(例如U[K]
,T
和U
时) }),人们可以看到某些东西是类型安全的,但是编译器却不能,因为编译器没有对通用类型执行正确的“高阶”推理。
因此,为此类函数提供类型签名的技术的一部分不仅是获得正确的约束,而且是编译器可以实际验证函数实现内部的类型安全性的约束。这并非总是可能的,因此有时您必须在实现内诉诸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
;您可以根据需要使用dogs
和type 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
参数上,它们或多或少告诉您问题出在哪里。是的。
虽然不确定K
和joinBy(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
再次。也许不是……到目前为止,代码是如此复杂,以至于我不愿意将其放在重要位置。可能会有一些奇怪的情况,很多人将看不到它在做什么。因此,备份时,我建议您使用真正对编译器友好的签名。
好的,希望能有所帮助;祝你好运!