网站搜索

typescript - 如何在对象属性之间正确推断类型?


我正在尝试为我的应用程序创建一个网格结构,并希望利用泛型根据单元格的名称正确推断其值的类型。我想要实现的推断在概念上类似于 TS 文档中的 setProperty 示例。

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

首先,先来看一下我定义的接口:

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">。

我猜我一定是在 Grid 中 columns 的返回类型和 Column 中 click 的参数类型之间搞砸了,尽管我不知道该如何解决这个问题,但我有一种强烈的感觉,这总得以某种方式工作,毕竟,我能够做像这样的事情,而且一切都能被完美推断出来:

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。

操场

答案#1

这是因为编译器会执行你告诉它做的事情。退一步看看泛型类型参数实际上意味着什么:它们是带有占位符的模板。你用 Grid<TestData> 注释了 myGrid 变量。因此,你告诉编译器 T 是 TestData。到目前为止,一切都好。

然后,你告诉它 columns 属性是一个返回通用接口数组的函数:Column<T, keyof T>[],这意味着这里的 keyof T 是 keyof TestData。现在,keyof 运算符返回的是一个 键的联合,这正是你得到的:"title" | "value"。

但你想要的是在数组元素和键之间建立一对一的关系,因此 Column<T, keyof T>[] 是不行的。你需要对键的联合类型进行 映射。所以,考虑到这一点,Grid 接口可以修改为这样:

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

然而,这还不足以获得所需的结果,因为数组的元素现在构成了对象的联合,这使它成为一个区分联合,从而使属性无法访问(因为只能访问共享属性)。

因此,您需要一种方法将联合类型创建为元组类型。这可不是一项漂亮的任务,因为联合类型是无序的;而且非常复杂(因为每增加一个新键,排列组合的数量都会急剧增加,参见此答案),所以请小心操作。一个实用工具可能如下所示:

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
          }
      }
    ],
  };
};

操场