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