-
-
Notifications
You must be signed in to change notification settings - Fork 544
Description
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
) : falseCheck 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 getterAs 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.