强类型表单生成器配置数组



我创建了一个表单生成器,它接受配置并呈现表单。基本结构看起来像这样:

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属性。

我假设您希望保持模型的确切结构。这意味着ab是简单的字段,但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

最新更新