首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >类型记录泛型约束查找两种类型共享的属性名称

类型记录泛型约束查找两种类型共享的属性名称
EN

Stack Overflow用户
提问于 2020-08-21 16:11:34
回答 1查看 722关注 0票数 0

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

代码语言:javascript
运行
复制
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共有的字符串。例如,给定以下类型:

代码语言:javascript
运行
复制
type Customer = {
  customerId: number,
  name: string,
  email?: string
}

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

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

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

代码语言:javascript
运行
复制
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。从这个吉特布问题中我知道,有时这个错误可能是一个假阳性,但我不确定这是否是其中之一,或者是否有更好的方法为K构建类型约束。

有没有更好的方法来做这件事,还是我只是看到一个假阳性?谢谢!

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2020-08-21 16:47:05

为了使您的操作是类型安全的,您需要确保不仅Kkeyof Tkeyof U中,而且T[K]U[K]是相同类型的(实际上,它们通过===是可比较的)。否则,你可能会发现自己接受了这类事情:

代码语言:javascript
运行
复制
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"); 

TreeDog都有一个名为bark的属性,但它们是不可比较的。

有一些方法可以收紧签名以要求T[K]U[K]兼容.事实上,有多种方法。

TS的一个重要警告是,在某些情况下,特别是在未指定泛型类型参数(如TUKjoinBy()实现中)的情况下,人类可以看到某些东西是类型安全的,但编译器不能,因为编译器没有对泛型类型执行正确的“高阶”推理。

因此,为这样的函数设计类型签名的部分技巧不仅是得到正确的约束,而且编译器可以验证函数实现内部的类型安全性。这并不总是可能的,因此有时您必须在实现中使用类型断言或类似的方法。

但是,对于您的函数,有一个“对编译器友好的”类型:

代码语言:javascript
运行
复制
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])
  })
}

在这里,编译器只需要关心data1数组的元素类型data1,并且key的类型K是它已知的键之一。然后,对data2的约束是,它必须是可分配给Pick<T, K>的某种类型的数组:也就是说,具有与T的属性兼容的K属性的类型。我们并不真正关心特定类型data2的元素是什么,因为我们只关注它的key属性;我们不返回该元素类型的任何值,因此我们不需要花费任何努力来推断它的类型参数U

编译器很乐意允许datum1[key] === datum2[key],因为它把它们都看作是T[K]类型(或者Pick<T, K>[K],幸运地被认为是兼容的)。

让我们确保它有效:

代码语言:javascript
运行
复制
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"和/或"bark"是错误的来源,而不是ordersdogs。这是可以实现的,但(可能)并不是编译器在实现中能够理解的方式。为了实现这一点,我将使用类似于类型断言的东西:单个调用签名过载:让调用签名对调用者来说是一个好签名,而实现签名对于实现来说是一个很好的签名:

代码语言:javascript
运行
复制
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;如果您愿意,可以将原来的签名保留在TK中,但在某种意义上,只要您愿意将调用端与实现端分开,就不重要了:在实现中,无论您想要什么,都要确保自己确信它的类型安全,然后给调用方一个符合您需要的签名。

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

代码语言:javascript
运行
复制
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参数上,它们或多或少地告诉您问题是什么。耶。

但是,不确定在TU之间实际上没有重叠的情况下,您想要做什么。以下应该是一个错误,但什么错误?

代码语言:javascript
运行
复制
joinBy(orders, trees, "name"); // error!
// -----------------> ~~~~~~
// Argument of type 'string' is not assignable to parameter of type 'never'.

我真的觉得trees是这里的错误,因为没有有效的key可以编写。为了实现这一点,您可以开始为joinBy()签名添加更多的复杂性.也许是这样:

代码语言:javascript
运行
复制
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

现在您在key上得到了错误,除非没有重叠,在这种情况下,data2上有错误

代码语言:javascript
运行
复制
joinBy(orders, trees, "name"); // error!
// ----------> ~~~~~
// Argument of type 'Tree[]' is not assignable to parameter of type 'SomeCompatibleType<Order>[]'.

又来了。也许不是..。到现在为止,代码太复杂了,我不愿意把它放在重要的地方。可能会有奇怪的边缘情况,很多人都看不清它在做什么。所以备份,编译器友好的签名可能是我真正建议的。

操场链接

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/63526387

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档