Skip to content

extendForm form is not assignable to withFieldGroup from the parent hook #2411

Description

@davit-micedesk

Describe the bug

A field group created with withFieldGroup from createFormHook rejects form={form} when form comes from extendForm on that same hook, even when the parent form values satisfy DeepKeysOfType.

extendForm only adds field/form components. Runtime is the same context and the same form API. The extra components are unused by the group. This should be a valid assignment (width subtyping). TypeScript rejects it because FieldComponent marks ExtendedApi as in out (invariant).

This is separate from value-level variance. Extra keys on the row (status, meeting_room_id, …) already work, as documented. Extra keys on the component map do not.

Your minimal, reproducible example

import { createFormHook, createFormHookContexts } from '@tanstack/react-form'

const { fieldContext, formContext } = createFormHookContexts()

function TextField() {
  return null
}

function ExtraField() {
  return null
}

const base = createFormHook({
  fieldContext,
  formContext,
  fieldComponents: { TextField },
  formComponents: {},
})

const extended = base.extendForm({
  fieldComponents: { ExtraField },
})

const Group = base.withFieldGroup({
  defaultValues: { name: '' },
  render: function Render({ group }) {
    return (
      <group.AppField name="name">{(field) => <field.TextField />}</group.AppField>
    )
  },
})

function App() {
  const form = extended.useAppForm({
    defaultValues: { name: '' },
  })

  // Type error on `form`.
  // A form from `base.useAppForm` with the same defaultValues typechecks.
  return <Group form={form} fields={{ name: 'name' }} />
}

Steps to reproduce

  1. Create a shared hook with createFormHook.
  2. Create withFieldGroup from that hook.
  3. Call extendForm and add one extra field or form component.
  4. Pass the extended form into the shared group.

Expected behavior

The extended form should be assignable to the parent hook's withFieldGroup.

extendForm spreads the parent component maps and reuses fieldContext / formContext:

https://github.com/TanStack/form/blob/main/packages/react-form/src/createFormHook.tsx

An extended form is the base form plus extra component keys. The group only uses the base keys (TextField in the repro). Extra components sit unused.

ExtendedApi only appears on the object passed into AppField's children:

children: (fieldApi: FieldApi<…> & ExtendedApi) => ReactNode

That is an output. The safe variance is out ExtendedApi:

  • AppField<Base & { ExtraField }> as AppField<Base>: the group only uses TextField. Sound.
  • AppField<Base> as AppField<Base & { ExtraField }>: the callback would read ExtraField and get undefined. Correctly rejected.

Today FieldComponent pins every slot, including ExtendedApi, as in out:

https://github.com/TanStack/form/blob/main/packages/react-form/src/useField.tsx

withFieldGroup then requires the exact TComponents / TFormComponents from the hook that created the group, so the extra keys fail.

TParentData staying invariant is right. The group reads and writes values. That is issue 1855, a different problem.

What a fix in TanStack looks like

The small change: in out ExtendedApi becomes out ExtendedApi on FieldComponent and the field-group equivalent.

The form prop can also be "at least these components":

form: AppFieldExtendedReactFormApi<
  TFormData,
  TOnMount,
  /* ... */,
  TComponents & Record<string, ComponentType<any>>,
  TFormComponents & Record<string, ComponentType<any>>
>

That only works if ExtendedApi is covariant. While it stays in out, no amount of & Extra on the call site will assign.

How often does this bug happen?

Every time

Platform

TypeScript 5.x / 6.x, React

TanStack Form adapter

react-form

TanStack Form version

1.33.2

TypeScript version

5.x / 6.x

Additional context

Related, not a duplicate: 1855 (value map too wide). That issue is about writing a wider field value into a narrower parent field. This issue is only the registered component map.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions