【问题标题】:How to use a generic object and key type as the key in a dictionary如何使用通用对象和键类型作为字典中的键
【发布时间】:2022-11-25 12:40:08
【问题描述】:

我正在寻找一个函数,该函数可以使用对象上的任何键过滤通用数组并返回唯一项。但是 Typescript 允许我将 T[Key] 用作数组而不是字典的键。

此代码有效:

const unique = <T, Key extends keyof T>(array: T[], key: Key): T[] => {
  var uniqueArray: T[Key][] = []
  var distinct: T[] = []

  for (var i = 0; i < array.length; i++) {
    if (!uniqueArray.includes(array[i][key])) {
      distinct.push(array[i])
      uniqueArray.push(array[i][key])
    }
  }

  return distinct
}

export default unique

此代码不起作用:

const unique = <T, Key extends keyof T>(array: T[], key: Key): T[] => {
  var uniqueDict: {[T[Key]]: number} = {}
  var distinct: T[] = []

  for (var i = 0; i < array.length; i++) {
    if (!uniqueDict[array[i][key]]) {
      distinct.push(array[i])
      uniqueDict[array[i][key]] = 1
    }
  }

  return distinct
}

export default unique

第二个代码示例在 var uniqueDict ... 行上给出了 A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.ts(1170) 的错误。

如果我把字典写成{[id: T[Key]]: number} = {},它也会在var uniqueDict ...行给出An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.ts(1337)的错误。

关于如何使独特方法的字典版本起作用有什么帮助吗?

【问题讨论】:

  • 您的代码存在多个问题;我可能会将其更改为 this version,它仅使用 Record&lt;string, number&gt; 而不是尝试将其设为 {[P in T[Key]]: number}(这就是您编写该类型的方式,但除非您有重要的理由这样做,否则这是不值得的Record&lt;string, number&gt;)。那能满足你的需求吗?如果是这样,我可以写一个答案来解释;如果没有,我错过了什么?
  • 这对我有用,但是它怎么知道 array[I][key] 将无限期地成为一个字符串?我想一个对象的所有键都可以假定为一个字符串,因为它们可以作为 obj['key'] 访问?这就是为什么它起作用的原因。我是打字稿的新手,我曾尝试使用Record,但我使用的是T[Key]类型而不是string,它在那里并不开心。
  • 我限制了T extends Record&lt;K, string&gt;,所以编译器知道array[i]T类型)在keyK类型)处有一个属性可以分配给string。当我写下我的答案时,我会解释更多。

标签: typescript typescript-generics


【解决方案1】:

第一个问题是这不是您要执行的操作的有效语法:

const unique = <T, K extends keyof T>(array: T[], key: K): T[] => {  
  var uniqueDict: { [T[K]]: number } = {} // syntax error

语法 { [xxx]: number } 表示一种类型,其中 xxxcomputed property name,因此必须对应于类似键的值。你不能使用类型根本没有,更不用说genericindexed access类型T[K]了。

正确的写法是使用mapped type

const unique = <T, K extends keyof T>(array: T[], key: K): T[] => {
  var uniqueDict: { [P in T[K]]: number } = {}; // error!    
  //                      ~~~~ <-- T[K] isn't known to be keylike

这相当于使用 the Record&lt;K, V&gt; utility typeRecord&lt;T[K], number&gt;


但是你看还是有问题;编译器不知道 T[K] 是类似键的类型。毕竟Tarray的元素类型,Kkey的类型,必然是T的key。所以 T[K] 是那个键的属性......但是那个属性本身是 string 吗?对于任意 TK,可能不是。

所以要解决这个问题,我们可以constrainT这样编译器就知道T[K]可以分配给string

const unique = <T extends { [P in K]: string }, K extends keyof T>(array: T[], key: K): T[] => {
  var uniqueDict: { [P in T[K]]: number } = {}; // error!    
  //  ~~~~~~~~~~ <-- {} isn't known to be assignable to this type

通过将T 约束为{[P in K]: string}(相当于Record&lt;K, string&gt;),[P in T[K]] 处的错误已清除。


但是我们仍然有一个问题:编译器不认为将一个空对象分配给一个变量是有效的,该变量的类型必须有一个属性,每个键的类型都是T[K]。编译器有道理;当您分配该空对象时,它没有这样的键。

在这一点上,有不同的方法可以进行。你可以对编译器和assert撒个小谎,说空对象有这些键:

var uniqueDict = {} as { [P in T[K]]: number }; // okay

或者你可以给uniqueDict一个indexed access type

var uniqueDict: { [k: string]: number } = {}; // okay

{ [k: string]: number } 类型表示具有一些任意 string 值键的对象,并且在存在的每个键处,值都是 number 类型。这足以让您的其余代码编译。请注意,默认情况下,编译器将允许您从任意 string 键读取数据,并且就好像某个属性肯定存在于那里一样:

uniqueDict.randomKey.toFixed(2); // no compiler error, but probably runtime error

只要小心不要在代码中做出此类假设,就可以了。否则,您可以打开 the --noUncheckedIndexedAccess compiler option 以更安全(但这更烦人并且影响的不仅仅是这一段代码),或者您可以显式地将 undefined 添加到 uniqueDict 的可能属性类型:

var uniqueDict: { [k: string]: number | undefined } = {}; // okay

但在这一点上,整个事情都编译好了,所以我们可以停下来。

Playground link to code

【讨论】:

    猜你喜欢
    • 2010-10-12
    • 2021-12-17
    • 2011-10-09
    • 2011-06-21
    • 2012-08-31
    • 1970-01-01
    • 2022-01-17
    • 1970-01-01
    • 2019-10-12
    相关资源
    最近更新 更多