Skip to content

withFieldGroup field map type is too wide #1855

@valerii15298

Description

@valerii15298

Describe the bug

When types of reusable form component are wider than types of parent form - it is not longer safe to use...

Your minimal, reproducible example

https://stackblitz.com/edit/tanstack-form-zwgf8aqd?file=src%2Findex.tsx

Steps to reproduce

Run this code with React.js and click X button

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

const { fieldContext, formContext } =
  createFormHookContexts()

const { useAppForm, withFieldGroup } = createFormHook({
  fieldComponents: {},
  formComponents: {},
  fieldContext,
  formContext,
})

type PasswordFields = {
  password: string | null
}

const reusableDefaultValues: PasswordFields = { password: '', }

export const FieldGroupPasswordFields = withFieldGroup({
  defaultValues: reusableDefaultValues,
  render: function Render({ group }) {
    return (
      <div>
        <h2>Reusable Fields</h2>
        <group.AppField name="password">
          {(field) => <>
            <input
              value={field.state.value ?? ""}
              onChange={(e) => field.handleChange(e.target.value)}
            />
            <button onClick={() => field.handleChange(null)}>X</button>
          </>}
        </group.AppField>
      </div>
    )
  },
})


// ======================= App Form ======================

type FormValues = {
  reusable: { password: string }
}

const defaultValues: FormValues = {
  reusable: { password: '', },
}

export function App() {
  const form = useAppForm({ defaultValues })

  return (
    <form.AppForm>
      <FieldGroupPasswordFields
        form={form}
        fields="reusable"
      />
      <form.Subscribe
        selector={f => f.values.reusable.password}
        children={p => <div>
          {
            // Here it CRASHES when password is null
          }
          {p.startsWith("1") ? "Valid" : "Invalid"}
        </div>}
      />
    </form.AppForm>
  )
}

Expected behavior

I should see the TypeScript error when types do not match exactly!

How often does this bug happen?

None

Screenshots or Videos

No response

Platform

Does not matter

TanStack Form adapter

react-form

TanStack Form version

1.25.0

TypeScript version

5.9.3

Additional context

At least ts check like this should be sufficient for this issue:

type Equals<T, S> =
	[T] extends [S] ? (
		[S] extends [T] ? true : false
	) : false

Check this out:

// this function just reads the value
function read(arg: string) {}
read("" as ""); // valid, narrower type
read("" as string); // valid, exact type
// type 'string | null' is not assignable to parameter of type 'string'
read("" as string | null); // invalid, too wide

// this function just writes the value, type checking works in inverse way
function write(setter: (arg: string) => void) {}
// type '(arg: "") => void' is not assignable to parameter of type '(arg: string) => void'
write((arg: "") => {}); // invalid, too narrow
write((arg: string) => {}); // valid, exact type
write((arg: string | null) => {}); // valid, wider type


// this function both reads and writes the value, type checking is strict
function readWrite(arg: string, setter: (arg: string) => void) {}
readWrite("" as "", (arg: "") => {}); // invalid, too narrow for the setter
readWrite("" as string, (arg: string) => {}); // valid, exact type
readWrite("" as string | null, (arg: string | null) => {}); // invalid, too wide for the getter

As you can see for the read getter function wider types are not accepted which is perfectly valid, but narrower type are accepted.

And for the write setter function, vice versa narrower type are not accepted but wider types are accepted, which is also perfectly logical.

The readWrite function only accepts exact type, which is kinda intersection of both types.

The FieldGroupPasswordFields component is basically readWrite function, it can read the form field values and write to them. So to maintain type safety it should show typescript error when parent form part does not have exactly the same types for the fields.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions