我试图创建一个函数,该函数接受一个对象数组,然后接受一个或多个参数,这些参数指定将在该数组上使用的属性.

例如,以下任一项:

update([{ name: 'X', percentage: 1, value: 2 }], 'percentage', 'value');
update([{ name: 'X', foo: 1, bar: 2 }], 'foo', 'bar');

try 1:

function update<T extends { name: string }, A extends keyof T, B extends keyof T>(data: T[], a: A, b: B) {
    for (const row of data) {
        console.log(row.name, row[a] * row[b]);
        row[a] = row[a] * row[b];
    }
}
  • 当我try 使用row[a]作为数字时,我得到了一个错误,因为类型是T[A],似乎它不知道这是一个数字
  • 当我试图给row[a]赋值时,我得到Type 'number' is not assignable to type 'T[A]'.(2322)

try 2:

type Entry<A extends string, B extends string> = { name: string } & Record<A | B, number>;

function update<T extends Entry<A, B>, A extends string, B extends string>(data: T[], a: A, b: B) {
    for (const row of data) {
        console.log(row.name, row[a] * row[b]);
        row[a] = row[a] * row[b];
    }
}
  • 这解决了使用row[a]时的问题,我似乎可以将其用作数字
  • 但当我try 将一个数字分配给row[a](或任何与此相关的数字)时,仍然得到Type 'number' is not assignable to type 'T[A]'.(2322)

在JS中,这似乎是一件相当常见的事情,但我不知道如何让Typescript理解它.

推荐答案

您肯定需要类似于Entry<A, B>的定义来表示已知在AB键处有number属性的内容.仅仅说ABkeyof typeof data是不够的,因为编译器都知道,keyof typeof data处可能有非数字属性(实际上,name属性是非数字的).

此外,您不希望datageneric类型T extends Entry<A, B>.constraint是一个上限,所以T可以是非常具体的东西,比如{name: string, pi: 3.14, e: 2.72},属性为number literal type.可分配给3.14类型的唯一值是3.14,因此编译器正确地阻止您将row[a] * row[b]分配给row[a].所以我们只希望dataEntry<A, B>型,而不是Entry<A, B>的某个未知亚型.这给了我们:

function update<A extends string, B extends string>(data: Entry<A, B>[], a: A, b: B) {
  for (const row of data) {
    console.log(row.name, row[a] * row[b]);  
    row[a] = row[a] * row[b]; // error!  Type 'number' is not assignable to type 'Entry<A, B>[A]'
  }
}

任务中有still breaks个.编译器无法看到number可分配给Entry<A, B>[A].十字路口有些地方让人困惑.我认为这是TypeScript的一个局限性(尽管有人可能会争辩说,如果A"name",那么Entry<A, B>[A]number & string类型,也称为never,因此编译器警告您number不一定可以分配给Entry<A, B>[A]是正确的,但在我看来,这很迂腐,在大多数情况下并不特别有用).我在GitHub中没有发现谈论它的特殊问题.

另一方面,编译器can看到number可指派给Record<A, number>[A].由于Entry<A, B>Record<A, number>的一个子类型,因此我们可以通过将其分配给一个新变量,并使用适当的type annotationrow安全地扩大到Record<A, number>(将上述"name"学究的数量进行模数化):

function update<A extends string, B extends string>(data: Entry<A, B>[], a: A, b: B) {
  for (const row of data) {
    console.log(row.name, row[a] * row[b]);
    const r: Record<A, number> = row; // okay
    r[a] = row[a] * row[b];
  }
}

现在一切正常:

update([{ name: 'X', percentage: 1, value: 2 }], 'percentage', 'value');
update([{ name: 'X', foo: 1, bar: 2 }], 'foo', 'bar');

Playground link to code

Typescript相关问答推荐

如何推断类方法未知返回类型并在属性中使用它

处理API响应中的日期对象

如何在函数参数中使用(或模仿)`success`的行为?

角形标题大小写所有单词除外

如何根据另一个属性的布尔值来缩小函数属性的返回类型?

如何在LucideAngular 添加自定义图标以及如何使用它们

如何表示多层深度的泛型类型数组?

如果数组使用Reaction-Hook-Form更改,则重新呈现表单

是否将自动导入样式从`~/目录/文件`改为`@/目录/文件`?

具有自引用属性的接口

有没有一种方法可以确保类型的成员实际存在于TypeScript中的对象中?

我在Angular 17中的environment.ts文件中得到了一个属性端点错误

Typescript 中相同类型的不同结果

如何键入派生类的实例方法,以便它调用另一个实例方法?

如何在具有嵌套数组的接口中允许新属性

如何在TypeScrip中使用数组作为字符串的封闭列表?

如何为对象数组中的每个条目推断不同的泛型

Svelte+EsBuild+Deno-未捕获类型错误:无法读取未定义的属性(读取';$$';)

如何确定单元格中的块对 mat-h​​eader-cell 的粘附(粘性)?

TypeScript 中的通用函数包装和类型推断