如何正确推断对象属性的类型?

How to correctly infer types across object properties?

我正在尝试为我的应用程序创建一个网格结构,并希望利用泛型根据其名称正确推断单元格值的类型。我试图实现的推论在概念上类似于 setProperty example in the TS docs.

我已经能够完成我想要实现的大部分目标,但我似乎已经到了我的大脑无法弄清楚需要最后一步的地步。

首先,这是我定义的接口:

interface Grid<T> {
  data: T[];
  columns: () => Column<T, keyof T>[];
}

interface Column<T, K extends keyof T> {
  name: K;
  click: (cell: Cell<T, K>) => void;
}

interface Cell<T, K extends keyof T> {
  row: T;
  column: K;
  value: T[K];
}

这就是我想用它们做的事情:

interface TestData {
  title: string;
  value: number;
}

const test = () => {
  const rows: TestData[] = [{ title: 'title', value: 1 }];

  const myGrid: Grid<TestData> = {
    data: rows,
    columns: () => [
      {
        name: 'title',
        click: (cell) => {
          // Expected:
          //   cell: Cell<TestData, "title">
          // Actual:
          //   cell: Cell<TestData, "title" | "value">
        },
      },
    ],
  };
};

所以正如你所看到的,类型推断在这里几乎完美地工作,但我只是无法弄清楚如何让 cell 的最后一点成为 Cell<TestData, "value"> 而不是Cell<TestData, "title" | "value">.

我猜我一定是在 Gridcolumns 的 return 类型和 click 中的参数类型之间搞砸了Column,尽管不知道如何解决这个问题,但我有一种强烈的感觉,这必须以某种方式起作用,因为毕竟,我能够做这样的事情,一切都得到完美的推断:

const testInference = <T, K extends keyof T>(data: Cell<T, K>) => undefined;

const row: TestData = { title: 'title', value: 1 };

testInference({
    row,
    column: 'title',
    value: 'bla',
});

所以问题本质上是如何从 name.

中正确推断出 click 参数的 K

Playground

这是因为编译器按照您的指示进行操作。退后一步,看看泛型类型参数的实际含义:它们是带有占位符的模板。您使用 Grid<TestData> 注释了 myGrid 变量。因此,您告诉编译器 TTestData。到目前为止一切顺利。

然后,你告诉它columns属性是一个函数returns一个泛型接口数组:Column<T, keyof T>[],意思就是这里的keyof Tkeyof TestData。现在,keyof 运算符 returns 一个 键的联合 ,这正是您得到的:"title" | "value".

但是您想要的是在数组元素和键之间建立一对一的关系,因此 Column<T, keyof T>[] 不会。您需要 map 键的并集。因此,考虑到这一点,可以将 Grid 接口修改为:

interface Grid<T> {
  data: T[];
  columns: () => { [P in keyof T] : Column<T, P> }[];
}

然而,这还不足以获得预期的结果,因为数组的元素现在构成了一个对象的并集,这使得它成为一个有区别的并集,使得属性无法访问(因为只有 shared 可以访问属性)。

因此,您需要一种从联合中创建 元组 类型的方法。考虑到工会是无序的,这不是一项漂亮的任务;这是一个相当爆炸性的(因为每个新密钥都会大大增加排列的数量,请参阅 ),因此请谨慎行事。实用程序可能如下所示:

type ToArr<T> = { [P in keyof T]: [T[P]?, ...Exclude<keyof T , P > extends never ? []: ToArr<Omit<T,P>> ] }[keyof T];

Grid接口对应修改为:

interface Grid<T> {
  data: T[];
  columns: () => ToArr<{ [P in keyof T] : Column<T, P> }>;
}

结果类型的行为符合预期:

const test = () => {
  const rows: TestData[] = [{ title: 'title', value: 1 }];

  const myGrid: Grid<TestData> = {
    data: rows,
    columns: () => [
      {
        name: 'title',
        click: (cell) => {
            cell.value.charCodeAt(0) //OK
        },
      },
      {
          name: "value",
          click: (cell) => {
              cell.value.toExponential(3) //OK
          }
      }
    ],
  };
};

Playground