T
TanStack5mo ago
wise-white

Optional fields

Hi guys, I have a question about Tanstack Form.
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters long'),
email: z.string().email('Invalid email address').optional(),
age: z.coerce.number().min(18, 'You must be at least 18 years old')
})

const TanstackFormExample = () => {
const form = useForm({
defaultValues: {
name: '',
email: '',
age: 12
},
validators: {
onChange: schema
},
onSubmit: ({ value }) => {
console.log(value)
}
})
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters long'),
email: z.string().email('Invalid email address').optional(),
age: z.coerce.number().min(18, 'You must be at least 18 years old')
})

const TanstackFormExample = () => {
const form = useForm({
defaultValues: {
name: '',
email: '',
age: 12
},
validators: {
onChange: schema
},
onSubmit: ({ value }) => {
console.log(value)
}
})
You can see that email is optional. But when I do it like this, the line onChange: schema starts firing an error sayng The types of ''~standard'.types' are incompatible between these types.. What is the best approach to handle optional fields? Someone suggested casting the default values like this:
defaultValues: {
name: '',
email: undefined,
age: 12
} as z.input<typeof schema>,
defaultValues: {
name: '',
email: undefined,
age: 12
} as z.input<typeof schema>,
But I don't really like casting and I feel like there should be a nicer solution to have some fields optional
12 Replies
adverse-sapphire
adverse-sapphire5mo ago
I don't really like casting
me neither but here it's the way to go
other-emerald
other-emerald5mo ago
satisfies instead of as gives you greater type safety for zod specifically, you can use the fact that input and output are different types to have a better experience with tanstack form
wise-white
wise-whiteOP5mo ago
Feels weird when of the selling points of Tanstack form was that you don't need to provide generics and yet you need to cast to make it work with optional fields Nope, satisfies does not help with this error
other-emerald
other-emerald5mo ago
casting is the short way. You could rewrite to an assertion if that's your major concern:
const defaultValues: z.input<typeof schema> = {
name: '',
email: '',
age: 12
}

const form = useForm({
defaultValues,
/* ... */
})
const defaultValues: z.input<typeof schema> = {
name: '',
email: '',
age: 12
}

const form = useForm({
defaultValues,
/* ... */
})
wise-white
wise-whiteOP5mo ago
Connected to this issue - when I remove the default values altogether:
const form = useForm({
validators: {
onSubmit: schema
},
onSubmit: ({ value }) => {
console.log(value)
}
})
const form = useForm({
validators: {
onSubmit: schema
},
onSubmit: ({ value }) => {
console.log(value)
}
})
The value inside onSubmit is of type unknown. I would expect it to infer the value based on the result of the validator. Am I doing something wrong or are default values required at all times?
other-emerald
other-emerald5mo ago
well, the validator ensures that your values match up. Unknown can be any value, so the validator doesn't complain. defaultValues tell the form how to populate its data, so you should always pass it
wise-white
wise-whiteOP5mo ago
Well yes, but sometimes you need some default values to be undefined. I would expect the form lib to infer the types based on the validator function, and not on the default values, as they are many times different from what I want in the output
other-emerald
other-emerald5mo ago
If the input values are different than your desired output values, that's what zod (and other standard schema libraries) can work well with:
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: z.getParsedType(schema),
received: z.ZodParsedType.null,
});
return z.NEVER;
}
return value;
});
}
/**
* Modifies the provided schema to be nullable on input, but non-nullable on output.
*/
export function nullableInput<TSchema extends ZodTypeAny>(schema: TSchema) {
return schema.nullable().transform((value, ctx: RefinementCtx) => {
if (value === null) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: z.getParsedType(schema),
received: z.ZodParsedType.null,
});
return z.NEVER;
}
return value;
});
}
selectedPerson: nullableInput(z.string())
selectedPerson: nullableInput(z.string())
wise-white
wise-whiteOP5mo ago
Yes, this proves my point - when I have it like this, the only type that is used is the InputType, and not the OutputType. I would expect that after submit validation, I would have the value being OutputType.
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters long'),
email: nullableInput(z.string().email('Invalid email address')),
age: z.coerce.number().min(18, 'You must be at least 18 years old')
})

type InputType = z.input<typeof schema>
type OutputType = z.infer<typeof schema>

const TanstackFormExample = () => {
const defaultValues: InputType = {
name: '',
email: null,
age: 12
}

const form = useForm({
defaultValues,
validators: {
onSubmit: schema
},
onSubmit: ({ value }) => {
----------------------------------------------------------------------
// Value is of type InputType, instead of OutputType
----------------------------------------------------------------------
}
})
const schema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters long'),
email: nullableInput(z.string().email('Invalid email address')),
age: z.coerce.number().min(18, 'You must be at least 18 years old')
})

type InputType = z.input<typeof schema>
type OutputType = z.infer<typeof schema>

const TanstackFormExample = () => {
const defaultValues: InputType = {
name: '',
email: null,
age: 12
}

const form = useForm({
defaultValues,
validators: {
onSubmit: schema
},
onSubmit: ({ value }) => {
----------------------------------------------------------------------
// Value is of type InputType, instead of OutputType
----------------------------------------------------------------------
}
})
other-emerald
other-emerald5mo ago
yes, that‘s the current implementation I think it was because field validators could override form level validators which could maybe cause conflicts? idk. Point is you get the input type on submit
wise-white
wise-whiteOP5mo ago
Shame, It would be so much nicer
other-emerald
other-emerald5mo ago
well, if you know how to implement it, a PR is always welcome

Did you find this page helpful?