How to Create Required or Explicitly Excluded Fields with TypeScript
source link: https://spin.atomicobject.com/2022/04/20/field-names-typescript/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
I have a React component that renders a contact information form (think email, phone, name, etc.). I want to require the user of my component to provide a name for all required fields unless the field is explicitly excluded.
Since the prop for the React component is just a TypeScript type, more broadly, I want each field to be either required or explicitly excluded.
Why I Want to Do This
My team is building out an application with lots of very similar forms that differ in subtle aspects. We’re using the React Hook Form library to build out our forms. React hook form requires that you supply a unique name
prop to every input component since this is what hooks up the input. So my contact form React component basically requires a bunch of field names and then takes care of rendering the form and passing those field names to a React hook form controller. I want to require all field names for my form component unless the field is explicitly excluded.
Starting Out: The Contact Component Requires All Field Names
Initially, I just required all field names to be provided. The ContactSection
component required the user to pass in field information of type RequiredFields<V>
where V
is the shape of our form data.
So RequiredFields<V>
has this shape:
[required contact field]: {name: Path<V> }
Here, Path
is the path to the field name.
And the arguments to my contact section looked like this:
type Props = {
// some unimportant props
fields: RequiredFields
}
This Prop type enforces that I would get a type error if I didn’t provide all the required field names to the ContactSection
. Let’s say name, phone, phoneExtension, and fax are all required field names.
This wouldn’t cause a type error:
ContactSection({
// other unimportant props
contactType: {
fields: {
name: { name: "primaryContact.name" },
phone: { name: "primaryContact.phone" },
phoneExtension: { name: "primaryContact.phoneExtension" },
fax: { name: "primaryContact.fax" },
},
},
})
But this would cause an error, since “contactType” is missing the key “fax.”
ContactSection({
// other unimportant props
contactType: {
fields: {
name: { name: "primaryContact.name" },
phone: { name: "primaryContact.phone" },
phoneExtension: { name: "primaryContact.phoneExtension" },
},
},
})
This is the behavior I want most of the time.
Modification: The Contact Component Requires All Field Names Unless Field Name Is Explicitly Excluded
Let’s say most of the time I want to show the name, phone, phoneExtension, and fax form fields. I like that I get a type error when I omit these fields because — chances are — I meant to include them but just forgot. But what about the few cases where I want to specifically not display a field? For example, I might not always want to show the “fax” field.
I want to be able to exclude a field if I choose to do so. When rendering the ContactSection
Component, it’s simple enough to not show the form field if explicitly excluded (if .exclude is true for a field, do not render field). Figuring out the type for the component props is a little trickier. I still want to get a type error if I simply forget to include the field but not if I explicitly exclude the field.
So, I built out a type that supports either including or explicitly excluding a given form field using a mapped type. I iterate through the keys of the required contact schema fields and say that the value of the keys must be either 1) included or 2) explicitly excluded by specifying {exclude: true}
.
The type definition for my ContactSection
props now looks like this:
ContactSection({
// other unimportant props
contactType: {
fields: {
[Property in keyof RequiredFields]:
| { exclude: true }
| { exclude: never} & RequiredFields[Property]
}
}
})
When using the ContactSection
component, I can now explicitly exclude a field from my type:
ContactSection({
// other unimportant props
contactType: {
fields: {
name: { exclude: true},
phone: { name: "primaryContact.phone" },
phoneExtension: { name: "primaryContact.phoneExtension" },
fax: { exclude:true},
},
},
})
Further Modification: The Contact Component Requires All Excludable Field Names Unless Field Name Is Explicitly Excluded
But what if I still want to provide some restrictions on which fields can be excluded? For instance, let’s say I know that the name should never ever be excluded.
In that case, I can create a type specifying which fields can be excluded. I’ll call that type ExcludableFields
. Then, if a field extends ExcludableFields
, I’ll make the value of that field either required or explicitly excluded (with {exclude:true}
). If the field doesn’t extend ExcludableFields
, I’ll always require a value for that field.
type ExcludableFields = "phoneExtension" | "fax"
contactType: {
fields: {
[Property in Exclude<
keyof RequiredFields,
ExcludableFields
>]: ContactSchemaRequiredFields[Property]
} & {
[Property in Extract, ExcludableFields>]:
| { exclude: true }
| (ContactSchemaRequiredFields[Property] & { exclude?: never })
}
}
With the updated type, this wouldn’t cause a type error:
ContactSection({
// props
contactType: {
fields: {
fax: { exclude: true },
phoneExtension: { exclude: true },
name: { name: "primaryContact.name" },
phone: { name: "primaryContact.phone" },
email: {
name: "primaryContact.email",
rules: { deps: ["contactsToReceiveEmails"] },
},
},
},
})
But this would:
ContactSection({
// other unimporant props
contactType: {
fields: {
fax: { exclude: true },
phoneExtension: { exclude: true },
name: {exclude: true},
phone: { name: "primaryContact.phone" },
email: {
name: "primaryContact.email",
rules: { deps: ["contactsToReceiveEmails"] },
},
},
},
})
Now the ContactSection
won’t let me accidentally forget to include a field. However, it still allows me to explicitly exclude fields that I’ve deemed as “excludable.”
I did have to work a little to define this type, but now the type will do the heavy lifting of ensuring I’m providing the correct Prop each time I build out a new contact form.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK