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);
}
};