我创建了一个表单生成器,它接受配置并呈现表单。基本结构看起来像这样:
const sampleConfig = {
meta: 'someMetaData',
fields: [
{
controlName: 'a',
type: 'text'
},
{
controlName: 'b',
type: 'select'
},
{
controlName: 'c',
type: 'subGroup',
fields: [
{
controlName: 'cA',
type: 'number'
}
]
}
]
}
这个配置将创建3个表单字段:一个文本字段,一个选择字段和一个带有数字字段的子组。您可以看到,每个字段既可以是一个控件,也可以有一个带有自己的字段数组的子组。
我已经强类型配置了,对于这个例子,让我们假设它是这样的:
interface Field {
controlName: string;
type: 'number' | 'select' | 'text' | 'subGroup'
fields?: Field[]
}
interface FormConfig {
meta: string
fields: Field[]
}
但是我想启用传递泛型来根据相应的模型验证controlName
。
对于这个例子,模型看起来像这样:
interface SampleModel {
a: string;
b: string;
c: {
cA: number;
}
}
理想情况下,我可以这样输入配置:
const sampleConfig: FormConfig<SampleModel> = { ... }
如果我的控件名称与键不对齐,它会抛出编译错误。我可以很容易地确保一个controlName匹配模型键之一,像这样:
interface Field<M, K extends keyof M> {
name: K;
type: 'select' | 'text' | 'number'
}
interface SubGroupField<M, K extends keyof M> extends Omit<Field<M, K>, 'type'> {
type: 'subGroup',
fields?: Field<M[K], keyof M[K]>[];
}
interface FormConfig<M, K extends keyof M> {
meta: 'string';
fields: Field<M, K>[] | SubGroupField<M[K], keyof M[K]>[];
}
虽然这并没有将配置与模型紧密绑定,但它只是确保控件名仅限于模型的键名,相同的controlName可以用于每个字段,只要它匹配模型的一个键名,Typescript就不会报错。
如果我将fields
属性作为对象而不是数组,并使用controlName
作为每个字段对象的键,这将非常容易完成。不幸的是,这个库已经存在一段时间了,如果可能的话,我需要避免重大的破坏性更改。
显然,这可能是有用的所有类型的场景,所以如果你有一个更好的问题的标题,我很乐意改变它,以帮助别人更容易找到它。我非常彻底地搜索了类似的问题,但没有找到任何,所以如果这个问题已经得到了回答,我也会更新到那里。
任何关于这方面的指导将不胜感激。谢谢!
编辑:包括来自@Linda Paise的建议,将'subGroup'隔离为自己的类型,强制只有这种类型可以拥有fields属性。
我假设您希望保持模型的确切结构。这意味着a
和b
是简单的字段,但c
必须是具有cA
字段的'subGroup'
。
如果我将
fields
属性作为对象而不是数组,并使用controlNames
作为每个字段对象的键,这将非常容易完成。不幸的是,这个库已经存在一段时间了,如果可能的话,我需要避免重大的破坏性更改。
基本上我们要做的是将对象映射到你所描述的字段的关键对象。然后将其转换为一个数组,该数组是该对象的所有值的并集。
type Values<T> = T[keyof T][];
与键控对象相比,数组确实有一些缺点,因为我们不会在缺少字段或重复字段时得到错误。但是对于无效的字段,我们确实会得到错误。
我们可以做得更好,而不仅仅是将fields
作为Field
的可选属性。我们可以说,如果键是模型的object
属性,那么字段必须是一个'subGroup'
,其字段匹配该对象的属性(注意,这将适用于数组,可能不是你想要的)。如果键是模型的一个基本值,那么type
是'number' | 'select' | 'text'
之一,没有fields
属性。
我们还说controlName
必须是键,而不是任意的string
。这允许我们在扁平版本中实现严格的类型。
把这些放在一起,我们得到这个:
type Values<T> = T[keyof T][];
type MapFields<Model> = {
[K in keyof Model]: Model[K] extends object ? {
controlName: K;
type: 'subGroup';
fields: Values<MapFields<Model[K]>>;
} : {
controlName: K;
type: 'number' | 'select' | 'text';
}
}
type Config<Model> = {
meta?: any;
fields: Values<MapFields<Model>>;
}
Typescript Playground Link