打字稿:keyof

时间:2019-06-12 08:58:03

标签: typescript

升级到TypeScript 3.5导致我的某些代码不再编译。我认为这是因为发生了重大变化:Generic type parameters are implicitly constrained to unknown。我正在尝试找到修复该代码的最佳方法。

TL; DR :如何声明类型为TK extends keyof T,但其中T[K]必须为字符串的泛型函数。

更长的版本:我想将对象数组转换为单个对象,该对象具有数组中的所有值,并以对象的某些属性为键。例如:

type Person = { id: string, firstName: string, lastName: string, age: number}

const array: Person[] = [
    {id: "a", firstName: "John", lastName: "Smith", age: 27},
    {id: "b", firstName: "Bill", lastName: "Brown", age: 53}
]

const obj = convertArrayToObject(array, "id")

其结果将是obj具有以下结构:

{ 
    a: {id: "a", firstName: "John", lastName: "Smith", age: 27},
    b: {id: "b", firstName: "Bill", lastName: "Brown", age: 53}
}

我具有此功能:

type ItemsMap<T> = { [key: string]: T }

function convertArrayToObject<T>(array: Array<T>, indexKey: keyof T): ItemsMap<T> {
    return array.reduce((accumulator: ItemsMap<T>, current: T) => {
        const keyObj = current[indexKey]
        const key = (typeof keyObj === "string") ? keyObj : keyObj.toString()
        accumulator[key] = current
        return accumulator
    }, {})
}

自从升级到TypeScript 3.5以来,在调用toStringProperty toString does not exist on type T[keyof T]时出错。

我能理解这个问题:由于TypeScript 3.5中的重大更改,current[indexKey]的返回值现在是unknown而不是对象,因此无法在其上调用toString 。但是我该如何解决呢?

理想情况下,我想对indexKey参数的类型施加通用约束,这样您就只能传递返回值本身就是字符串的键。这是到目前为止我已经设法做到的(尽管我不确定这是否是最好的方法):

首先,我声明一个用于查找给定类型TObj的所有属性的类型,这些属性返回给定类型的结果TResult

type PropertiesOfType<TObj, TResult> =
    { [K in keyof TObj]: TObj[K] extends TResult ? K : never }[keyof TObj]

因此,例如,我现在可以获得Person的所有字符串属性:

type PersonStringProps = PropertiesOfType<Person, string> // "firstName" | "lastName" | "id"

现在我可以如下声明函数了:

function convertArrayToObject<T, K extends PropertiesOfType<T, string>>(
    array: Array<T>, indexKey: K): ItemsMap<T> { ...

我现在只能使用返回字符串的属性来调用该函数,例如:

convertArrayToObject(array, "id") // Compiles, which is correct
convertArrayToObject(array, "age") // Doesn't compile, which is correct

但是,在函数体中,我似乎仍然无法使用传入的keyof T并使编译器知道返回的值是一个字符串:

return array.reduce((accumulator: ItemsMap<T>, current: T) => {
    const key: string = current[indexKey]

无法编译:{{1​​}}。我可以通过以下方法解决这个问题:

Type T[K] is not assignable to type string

我猜这很安全,因为我知道const key: string = current[indexKey] as unknown as string 是一个字符串。但这似乎还不太正确。

1 个答案:

答案 0 :(得分:2)

您可以通过将keyObj.toString()调用更改为String(keyObj)来轻松解决此问题,无论您通过什么调用,它都会在内部调用.toString(),因此行为不会改变,只是不会爆炸在undefinednull上。实际上,您可以替换整行:

const key = (typeof keyObj === "string") ? keyObj : keyObj.toString()

使用

const key = String(keyObj)

因为它是字符串,所以什么也不做。

更新:

您几乎拥有正确的类型安全解决方案,只需要对T施加额外的约束:

function convertArrayToObject<
  T extends { [Key in K]: string }, // This is required for the reduce to work
  K extends PropertiesOfType<T, string>
>(array: Array<T>, indexKey: K): ItemsMap<T> {
  return array.reduce((accumulator: ItemsMap<T>, current: T) => {
    accumulator[current[indexKey]] = current
    return accumulator
  }, {})
}