I have form fields represented as objects that get mapped over and based upon the type
returns React elements:
import { FieldProps } from "~types"; const fields: FieldProps[] = [ { type: "text", name: "username", value: "", required: true, ...other props }, { type: "password", name: "password", value: "", required: true, ...other props }, ...etc ] export default fields;
The problem I’m running into is I’m trying to validate the fields after a form is submitted and check for any errors in value
:
handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); const { validatedFields, errors } = fieldValidator(this.state.fields); if(errors) { this.setState({ errors }); return; } ...etc }
but this reusable validate function has a ts error:
import isEmpty from "lodash.isempty"; /** * Helper function to validate form fields. * * @function * @param {array} fields - an array containing form fields. * @returns {object} validated/updated fields and number of errors. * @throws {error} */ const fieldValidator = < T extends Array<{ type: string; value: string; required?: boolean }> >( fields: T, ): { validatedFields: T; errors: number } => { try { if (isEmpty(fields)) throw new Error("You must supply an array of form fields to validate!"); let errorCount: number = 0; // this turns the "validatedFields" array into an { errors: string; type: string; name: // string; value: string; required?: boolean | undefined;}[] type, when it needs to be "T", // but "T" won't be inferred as an Array with Object props: type, value, required, value defined within it const validatedFields = fields.map(field => { let errors = ""; const { type, value, required } = field; if ((!value && required) || (isEmpty(value) && required)) { errors = "Required."; } else if ( type === "email" && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(field.value) ) { errors = "Invalid email."; } if (errors) errorCount += 1; return { ...field, errors }; }); return { validatedFields, errors: errorCount }; // TS error here } catch (err) { throw String(err); } }; export default fieldValidator;
Since validatedFields
turns into:
{ errors: string; name: string; value: string; type: string; required: boolean; }[]
and it’s returned { validatedFields, errors }
, it throws this TS error:
Type '{ errors: string; type: string; value: string; required?: boolean | undefined; }[]' is not assignable to type 'T'. '{ errors: string; type: string; value: string; required?: boolean | undefined; }[]' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ type: string; value: string; required?: boolean | undefined; }[]'.ts(2322) index.ts(17, 6): The expected type comes from property 'validatedFields' which is declared here on type '{ validatedFields: T; errors: number; }'
Is there a way to infer T
as an array of objects that expects at least 4 (or more) properties, but returns the same typed array (just with an updated errors
property)?
Advertisement
Answer
I managed to figure it out by extending T
to any[]
and then defining the result of validatedFields as T
to preserve the passed in types:
const fieldValidator = < T extends any[] >( fields: T, ): { validatedFields: T; errors: number } => { try { if (!fields || fields.length < 0) throw new Error("You must supply an array of fields!"); let errorCount: number = 0; const validatedFields = fields.map(field => { let errors = ""; const { type, value, required }: Pick<BaseFieldProps, "type" | "value" | "required"> = field; if ((!value && required) || ((value && value.length < 0) && required)) { errors = "Required."; } else if ( type === "email" && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(value) ) { errors = "Invalid email."; } if (errors) errorCount += 1; return { ...field, errors }; }) as T; return { validatedFields, errors: errorCount }; } catch (err) { throw String(err); } };