问题描述
我正在尝试为我的应用程序创建一个网格结构,并希望利用泛型根据名称正确推断单元格值的类型。我试图实现的推理在概念上类似于 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
的返回类型和 Grid
中 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',});
中正确推断出 K
参数的 click
。
解决方法
这是因为编译器会按照您的指示去做。退一步看看泛型类型参数的实际含义:它们是带有占位符的模板。您使用 myGrid
注释了 Grid<TestData>
变量。因此,您告诉编译器 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> }[];
}
然而,这还不足以获得想要的结果,因为数组的元素现在构成了对象的联合,这使得它成为一个有区别的联合,使得属性无法访问(因为只有共享属性可以访问).
因此,您需要一种从联合创建元组类型的方法。考虑到联合是无序的,这不是一项很好的任务;和一个非常爆炸性的(因为每个新键都会大大增加排列的数量,请参阅 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
}
}
],};
};