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

问题描述

我正在尝试为我的应用程序创建一个网格结构,并希望利用泛型根据名称正确推断单元格值的类型。我试图实现的推理在概念上类似于 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"> 而不是 {{ 1}}。

我的猜测是我一定在 Cell<TestData,"title" | "value">columns 的返回类型和 Gridclick 的参数类型之间的某个地方搞砸了,尽管不知道如何解决这个问题,但我有一种强烈的感觉,这必须以某种方式起作用,因为毕竟,我能够做这样的事情,一切都得到完美推断:

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',}); 中正确推断出 K 参数的 click

Playground

解决方法

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

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

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

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

然而,这还不足以获得想要的结果,因为数组的元素现在构成了对象的联合,这使得它成为一个有区别的联合,使得属性无法访问(因为只有共享属性可以访问).

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...