I'm using RHF and trying to create a custom form component that takes an array of Fields and then renders an input for each Field. For example, it might take as input something like this:
const fields = [
{
type: 'select',
name: 'data',
label: 'Location/Date',
options: [
{ name: 'a', color: 'blue' },
{ name: 'b', color: 'green' },
{ name: 'c', color: 'purple' },
],
getDisplayText: (item) =>
,
},
{
type: 'text',
name: 'harvestSchemaId',
label: 'Harvest Schema ID',
validation: {
required: 'Harvest Schema ID is required',
},
},
{
type: 'text',
name: 'pinId',
label: 'Pin ID',
validation: {
required: 'Pin ID is required',
},
},
]
Then the component maps over all of the inputs and renders either an or , based on field.type. The issue I'm running into is that my select type takes generic options:
interface BaseField {
name: string
label?: string
validation?: RegisterOptions
}
interface InputField extends BaseField {
type: 'text' | 'email' | 'password'
}
interface SelectField<T> extends BaseField {
type: 'select'
options: T[]
getDisplayText?: (item: T) => string
}
Because the getDisplayText callback needs to have the same type as options. My question is, how in the world can I type everything such that I have type safety? I've tried typing Fields like this:
const fields: Field<{ name: string; color: string }>[] = ...
But this only works if there's one select. If there's more than one, I have to do a union:
const fields: Field<{ name: string; color: string } | { test: string }>[] = ...
And then I lose all type safety for my getDisplayText callback. Also, I know I could just do it like this:
const fields: [
SelectField<{ name: string; color: string }>,
InputField,
InputField,
] = ...
But I feel like there's got to be a better way, and I'm trying to make this as reusable and non-boilerplatey as possible. What's a good way to do this? I know this has got to be a pretty common use case, but I can't find anything online. Thanks for any help!
I'm using RHF and trying to create a custom form component that takes an array of Fields and then renders an input for each Field. For example, it might take as input something like this:
const fields = [
{
type: 'select',
name: 'data',
label: 'Location/Date',
options: [
{ name: 'a', color: 'blue' },
{ name: 'b', color: 'green' },
{ name: 'c', color: 'purple' },
],
getDisplayText: (item) =>
,
},
{
type: 'text',
name: 'harvestSchemaId',
label: 'Harvest Schema ID',
validation: {
required: 'Harvest Schema ID is required',
},
},
{
type: 'text',
name: 'pinId',
label: 'Pin ID',
validation: {
required: 'Pin ID is required',
},
},
]
Then the component maps over all of the inputs and renders either an or , based on field.type. The issue I'm running into is that my select type takes generic options:
interface BaseField {
name: string
label?: string
validation?: RegisterOptions
}
interface InputField extends BaseField {
type: 'text' | 'email' | 'password'
}
interface SelectField<T> extends BaseField {
type: 'select'
options: T[]
getDisplayText?: (item: T) => string
}
Because the getDisplayText callback needs to have the same type as options. My question is, how in the world can I type everything such that I have type safety? I've tried typing Fields like this:
const fields: Field<{ name: string; color: string }>[] = ...
But this only works if there's one select. If there's more than one, I have to do a union:
const fields: Field<{ name: string; color: string } | { test: string }>[] = ...
And then I lose all type safety for my getDisplayText callback. Also, I know I could just do it like this:
const fields: [
SelectField<{ name: string; color: string }>,
InputField,
InputField,
] = ...
But I feel like there's got to be a better way, and I'm trying to make this as reusable and non-boilerplatey as possible. What's a good way to do this? I know this has got to be a pretty common use case, but I can't find anything online. Thanks for any help!
Share Improve this question asked Nov 20, 2024 at 0:43 GMoneyGMoney 435 bronze badges1 Answer
Reset to default 1One way I have solved this problem is defining a no-op helper function for type checking.
function createField(field: InputField): BaseField;
function createField<T>(field: SelectField<T>): BaseField;
function createField(field: BaseField) {
return field;
}
createField
is a function with multiple overloads for each of the BaseField
types you have, and it will just return the same object, with type BaseField
. It will force typescript to type-check its input, to actually be an InputField
or a SelectField<T>
.
Then you can use it like this:
const fields: BaseField[] = [
createField({
type: 'select',
name: 'data',
label: 'Location/Date',
options: [
{ name: 'a', color: 'blue' },
{ name: 'b', color: 'green' },
{ name: 'c', color: 'purple' },
],
getDisplayText: (item) => item.name
}),
createField({
type: 'text',
name: 'harvestSchemaId',
label: 'Harvest Schema ID',
validation: {
required: 'Harvest Schema ID is required',
},
}),
createField({
type: 'text',
name: 'pinId',
label: 'Pin ID',
validation: {
required: 'Pin ID is required',
},
}),
];