Requiring or Explicitly Excluding Component Properties with TypeScript

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.