英文:
Advanced Generic Types lenght of Array has to match amount of keys in related object
问题
以下是您要翻译的内容:
"Is there a possibility to create type safety across rows and columns where one object defines the structure of the other one?"
初始情况:
export interface TableColumn {
name: string;
type: "string" | "number" | "action"; // this type is an arbitrary type and is needed for default rendering so we know how to display it.
id: string;
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: any[]; // <-- Goal would be to get rid of any
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
现在行需要匹配列中定义的结构,如下所示的示例。我还尝试在TableRowProps接口中添加可选参数,如果需要,可以添加到每行中。
// These props are optional but can be added to any Row in PaginatedTableProps.rows
interface TableRowProps {
rowType?: "Basic" | "Master";
}
export interface TableColumn {
name: string;
type: "string" | "number" | "action"; // this type is an arbitrary type and is needed for default rendering so we know how to display it.
id: string;
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: Array<TableRowProps | Record<string, unknown>>;
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
// Example use:
const props: PaginatedTableProps = {
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3" }
],
rows: [
{f: "foo", count: 12345, z: "bar"},
{f: "zig", count: 6789}, // should mark an error that z is missing
{f: "bar", count: "123", z: "far"}, // should mark an error that count is not a number
{f: "bas", count: 123, z: "asdf", rowType: "Basic"}, // ok
{f: "bas", count: 123, z: "asdf", rowType: "Expert"} // should be wrong as the rowType needs to match Basic or Master
]
}
编辑:我遗漏了第一个示例中的额外的CellRenderer,这将是添加类型安全性的下一步:
interface TableRowProps {
rowType?: "Basic" | "Master";
}
export interface TableColumn {
name: string;
type: "string" | "number" | "action";
id: string;
renderFunction?: (data: any) => string; // any should match the Type selected in Types maybe this is impossible.
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: Array<TableRowProps | Record<string, unknown>>;
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
// Example use:
const props: PaginatedTableProps = {
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3", renderFunction: (s: string) => s}
],
rows: [
{f: "foo", count: 12345, z: "bar"},
{f: "zig", count: 6789}, // should mark an error that z is missing
{f: "bar", count: "123", z: "far"}, // should mark an error that count is not a number
{f: "bas", count: 123, z: "asdf", rowType: "Basic"}, // ok
{f: "bas", count: 123, z: "asdf", rowType: "Expert"} // should be wrong as the rowType needs to match Basic or Master
]
}
英文:
Is there a possibility to create type safety across rows and columns where one object defines the structure of the other one?
Initial Situation:
export interface TableColumn {
name: string;
type: "string" | "number" | "action"; // this type is an arbitrary type and is needed for default rendering so we know how to display it.
id: string;
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: any[]; // <-- Goal would be to get rid of any
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
Now the rows need to match the structure defined in the columns seen the example below. I also try to add optional Parameter over the TableRowProps interface, those can be added to each row if needed.
// These props are optional but can be added to any Row in PaginatedTableProps.rows
interface TableRowProps {
rowType?: "Basic" | "Master";
}
export interface TableColumn {
name: string;
type: "string" | "number" | "action"; // this type is an arbitrary type and is needed for default rendering so we know how to display it.
id: string;
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: Array<TableRowProps | Record<string, unknown>>;
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
// Example use:
const props: PaginatedTableProps = {
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3", }
],
rows: [
{f:"foo", count: 12345, z: "bar"},
{f:"zig", count: 6789}, // should mark an error that z is missing
{f:"bar", count: "123", z: "far"}, // should mark an error that count is not a number
{f:"bas", count: 123, z: "asdf", rowType:"Basic"}, // ok
{f:"bas", count: 123, z: "asdf", rowType:"Expert"} // should be wrong as the rowType needs to match Basic or Master
]
}
EDIT: I omitted the additional CellRenderer from the first example this would be a next step to add type safety to here:
interface TableRowProps {
rowType?: "Basic" | "Master";
}
export interface TableColumn {
name: string;
type: "string" | "number" | "action";
id: string;
renderFunction?: (data: any) => string; // any should match the Type selected in Types maybe this is impossible.
}
export interface PaginatedTableProps {
/** `rows` data must match the "schema" provided in `columns` */
rows: Array<TableRowProps | Record<string, unknown>>;
/** `columns` define the schema for the table and data provided in `rows` must match */
columns: TableColumn[];
}
// Example use:
const props: PaginatedTableProps = {
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3", renderFunction: (s: string) => s}
],
rows: [
{f:"foo", count: 12345, z: "bar"},
{f:"zig", count: 6789}, // should mark an error that z is missing
{f:"bar", count: "123", z: "far"}, // should mark an error that count is not a number
{f:"bas", count: 123, z: "asdf", rowType:"Basic"}, // ok
{f:"bas", count: 123, z: "asdf", rowType:"Expert"} // should be wrong as the rowType needs to match Basic or Master
]
}
答案1
得分: 1
为了使这个工作起效,我们需要建立一个映射,将TableColumn
中type
属性的字符串文字类型与实际数据类型关联起来。幸运的是,有一种简单的方法可以描述字符串文字类型与某个任意类型之间的映射:使用interface
。让我们称其为TypeMap
:
interface TypeMap {
string: string;
number: number;
action: (x: string) => void; // 谁知道
}
(我不知道“action
”是什么,所以🤷♂️)。然后,您可以根据TypeMap
来定义TableColumn
:
type TableColumn = { [P in keyof TypeMap]: {
name: string;
type: P;
id: string;
renderFunction?: (data: TypeMap[P]) => string;
} }[keyof TypeMap];
在解释它的工作原理之前,让我们确保它是有用的:
/* type TableColumn = {
name: string;
type: "string";
id: string;
renderFunction?: ((data: string) => string) | undefined;
} | {
name: string;
type: "number";
id: string;
renderFunction?: ((data: number) => string) | undefined;
} | {
name: string;
type: "action";
id: string;
renderFunction?: ((data: (x: string) => void) => string) | undefined;
} */
因此,TableColumn
是一个有歧义的联合类型,其中type
是辨别属性,renderFunction
方法的参数类型取决于该type
属性。
好的,它是如何工作的:TableColumn
被创建为mapped type,我们立即在其中进行索引,以获得其属性值的联合类型。
现在,在TypeScript中并没有真正满足您需求的特定PaginatedTableProps
类型,其中rows
和columns
属性在正确的方式上受到约束。相反,我们需要定义一个泛型PaginatedTableProps<T>
类型,其中T
对应于columns
属性的类型,并且rows
是依赖于T
的某种类型。它可能看起来像这样:
interface PaginatedTableProps<T extends readonly TableColumn[]> {
columns: T;
rows: Array<{
[I in keyof T & `${number}` as T[I]["name"]]: TypeMap[T[I]["type"]]
} & TableRowProps>;
}
rows
属性是一个Array
类型,其元素的类型是TableRowProps
与一个映射类型的交集,该映射类型将T
中的TableColumn
子类型数组转换为对象类型,其键被重新映射为TableColumn
的name
属性,属性是通过查找TypeMap
接口中的相应type
属性而获得的。
像PaginatedTableProps<T>
这样的泛型类型直接在变量声明上进行注释是很烦人的,因为您必须手动指定类型参数T
,这可能会非常冗长和重复。
相反,我们可以创建一个泛型辅助函数paginatedTableProps()
,它从其参数中推断类型参数T
,并将其原样返回,如下所示:
const paginatedTableProps = <const T extends readonly TableColumn[]>(
ptp: PaginatedTableProps<T>) => ptp;
(请注意,我们使用const
类型参数来告诉编译器尝试为T
推断非常具体的类型……毕竟,我们关心rows
元素的name
属性的字符串文字类型。)
因此,您可以使用const p = paginatedTableProps({ ⋯ })
来代替写const p: PaginatedTableProps = { ⋯ }
(这是无法做的,因为PaginatedTableProps
是泛型),或写const p: PaginatedTableProps< ⋯ > = { ⋯ }
(这很繁琐和冗余)的方式,编译器会根据需要推断出p
的类型。
好的,让我们试一下:
const p = paginatedTableProps({
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3", renderFunction: (s: string) => s }
],
rows: [
{ f: "foo", count: 12345, z: "bar" }, // 正确
{ f: "zig", count: 6789 }, // 错误
//~~~~~~~~~~~~~~~~~~~~~~~ <--- 缺少了 z
{ f: "bar", count: "123", z: "far" }, // 错误
// ~~~~~ <--- 字符串不是数字
{ f: "baz", count: 123, z: "asdf", rowType: "Basic" }, // 正确
{ f: "qux", count: 123, z: "ghjk", rowType: "Expert" }
<details>
<summary>英文:</summary>
In order for this to work we'll need to set up a mapping between the string [literal types](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) of the `type` property in `TableColumn` to the actual data type. Luckily there's an easy way to describe a mapping between a string literal type and some arbitrary type: an [`interface`](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#interfaces). Let's call it `TypeMap`:
interface TypeMap {
string: string;
number: number;
action: (x: string) => void; // who knows
}
(I don't know what an "`action`" is so 🤷♂️). Then you can define `TableColumn` in terms of `TypeMap`:
type TableColumn = { [P in keyof TypeMap]: {
name: string;
type: P
id: string;
renderFunction?: (data: TypeMap[P]) => string;
} }[keyof TypeMap]
Before explaining how that works, let's make sure it's something useful:
/* type TableColumn = {
name: string;
type: "string";
id: string;
renderFunction?: ((data: string) => string) | undefined;
} | {
name: string;
type: "number";
id: string;
renderFunction?: ((data: number) => string) | undefined;
} | {
name: string;
type: "action";
id: string;
renderFunction?: ((data: (x: string) => void) => string) | undefined;
} */
So `TableColumn` is a [discriminated union type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) with `type` as the discriminant property, and where the parameter type of the `renderFunction` method depends on that `type`.
Okay, so how it works: `TableColumn` is created as a *distributive object type* as coined in [microsoft/TypeScript#47109](https://github.com/microsoft/TypeScript/pull/47109). It's a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) into which we immediately [index](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) in order to get a [union](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types) of its property values.
----
Now, there's not really a *specific* `PaginatedTableProps` type in TypeScript that meets your needs, whereby the `rows` and `columns` properties are constrained to each other in the right way. Instead we'll need to define a [generic](https://www.typescriptlang.org/docs/handbook/2/generics.html) `PaginatedTableProps<T>` type, where `T` corresponds to the type of the `columns` property, and where `rows` is some type that depends on `T`. It could look like this:
interface PaginatedTableProps<T extends readonly TableColumn[]> {
columns: T;
rows: Array<{
[I in keyof T & `${number}` as T[I]["name"]]: TypeMap[T[I]["type"]]
} & TableRowProps>;
}
The `rows` property is an `Array` type whose elements' type is the [intersection](https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types) of `TableRowProps` with a mapped type that turns the array of `TableColumn` subtypes from `T` into an object type whose keys are [remapped](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as) to the `name` property of the `TableColumn`s, and whose properties are what you get when you look the corresponding `type` property up in the `TypeMap` interface.
----
Generic types like `PaginatedTableProps<T>` are annoying to [annotate](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-annotations-on-variables) directly on variable declarations because you have to specify the type argument `T` manually, and that could be quite verbose and repetitive.
Instead we can make a generic helper function `paginatedTableProps()` that *infers* the type argument `T` from its argument and returns it as-is, like this:
const paginatedTableProps = <const T extends readonly TableColumn[]>(
ptp: PaginatedTableProps<T>) => ptp;
(Note that we use a [`const` type parameter](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#const-type-parameters) to tell the compiler to try to infer very specific types for `T`... we care about the string literal types of the `name` properties of `rows` elements, after all.)
And so instead of writing `const p: PaginatedTableProps = { ⋯ }` which you can't do because `PaginatedTableProps` is generic; and instead of writing `const p: PaginatedTableProps< ⋯ > = { ⋯ }` which is tedious and redundant; you write `const p = paginatedTableProps({ ⋯ })` and you'll see that `p` is inferred as desired.
---
Okay, let's try it out:
const p = paginatedTableProps({
columns: [
{ name: "f", type: "string", id: "1" },
{ name: "count", type: "number", id: "2" },
{ name: "z", type: "string", id: "3", renderFunction: (s: string) => s }
],
rows: [
{ f: "foo", count: 12345, z: "bar" }, // okay
{ f: "zig", count: 6789 }, // error,
//~~~~~~~~~~~~~~~~~~~~~~~ <-- z is missing
{ f: "bar", count: "123", z: "far" }, // error
// ~~~~~ <-- string isn't a number
{ f: "baz", count: 123, z: "asdf", rowType: "Basic" }, // okay
{ f: "qux", count: 123, z: "ghjk", rowType: "Expert" } // error,
// ------------------------------> ~~~~~~~ not "Basic" or "Master"
]
})
That looks like the behavior you want. The compiler knows that the `columns` property elements must be of type `{f: string, count: number, z: string, rowType?: "Basic" | "Master"}`, and you get errors accordingly if you mess up. And the type of `p` is inferred as
/* const p: PaginatedTableProps<readonly [{
readonly name: "f";
readonly type: "string";
readonly id: "1";
}, {
readonly name: "count";
readonly type: "number";
readonly id: "2";
}, {
readonly name: "z";
readonly type: "string";
readonly id: "3";
readonly renderFunction: (s: string) => string;
}]> */
which captures the entire `rows` structure and would be quite annoying to write out... which means it's a good thing it was inferred for us!
[Playground link to code](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgCoE8AOECydPIDeAUMmcgM5hSgDmAXJdXQNynkgCuAtgEbSMufaG3LJEYYAHsQjABQAPRlRohaASmQBeAHzIAblOAATFsgD055AHcAFlOQBrEFOsViAX2LEwWFKjheABsIAGEpIJ4QbSJkAG0ABWRQJwh0KRg0PzxMAF1GEjEQOG4IZWY1UXJfbEYE9jITctVaKrIoCBBjaAAxThAESRkAfnljODA4RgxsHMTczV0mFrYPZA84xzSMrNn8XO9icwAqZBr-QJDwyO5orSIG5GLS5tZH88YAIhU6T7bk4yvSqPDpdXr9QbSECjZByOTjSZAjTaPQ-NSaAA+yH63RgoAgpk8yCxhQ4JTKyzeYg+yE+Qn4UD+jyalOBYlB3SgfQGQ2h8nhEymTx4DMWqIqyKxOIgeJABNWxIeRXJSP+NM+EihTLELLRrRBnU53MhI35CKFiiRYoMRmM1r1mOxYNl8qJx3Mh1AkFgiAuwQgACVXAkoFJMBQleRQ9YZhAYZ8AEJwCjABCfRWfPBUaBMrzEL3QeBIZAJOC0UATAkBf0hsMUAA8qGQEAUkC6EY6cGMMiC6DQlzCESicVyelJZAQQ9uFGm-2jM+QAEEoFA4Oh6+OxPEAJLJaJbdKZJsAMmQAAMACSEenQDxn8QR1Bxbe5OJ08mfXL5Xa4fBxJ8vm+5yfgcYhrKe1YhEG1i1uGOirN4k4gFQyCYGWFaQMYkEQLBEb3PWSEoU2LZtsYHYQF2PZ9th1zDqOciPJgYCYHU6HFJh2G4Y2OjWkxmBsMQhFgKhMRoeW7FVgOuFyJuk43MhjBxI8YiEE8Kq0jAnwADRnH4Xx6tpAJfAAjOmHhacp5Cqc8FKfJO-RgIZ6o3oyOksp8ABMZkWVuVlqS8tIAF5OXptIGW5gK0gAzIZHLgjyULyAuDoopQ6yPLkPlRq4C5Kb5ZCqTAXwwFIUiGfZ4CMMZHlRQALAArDpgVfLwcCMusOmWMgUiOGulkFcgRVBcAtDlVIDmMAAbAA7AAHAAnB1FhWNAoZQFlvmWAAfjtu17ftB07cg9YALQncggXJBG3DABQKZqP1sRDZ8rWucgFVgCZNWGc1Gltd5y3NiuUhQI9XX5WQ+3HWdrK0FdIAAOTCXAwrCKDEOFS1cDBTpH1VTVTVfMmxiaTp0axl8SYpmmS1dT1fUY4NXwAI6cAoY0Tcg1VRYTtK0LYABWjixa4FO0gAogo2BQI56yA6tIMbVuXVnaravqxrmuq3oh1PFIwmJsmqbpiDtJZt6nwZZ46gCSc70yChLElmxlZYVJobhvWnbdiAvbxJu3tUf5tmaXOFE+376oGWHlG+327mmasOkB+HQc2V8H3alGqdx7ptS0i5WftDnfvuV5SeRsXsd++nQVF8gge51HEr143peRZ8MUxxHfZxVyEK8klVqpXqqyjsg7pAA)
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论