I have form fields represented as objects that get mapped over and based upon the type
returns React elements:
JavaScript
x
10
10
1
import { FieldProps } from "~types";
2
3
const fields: FieldProps[] = [
4
{ type: "text", name: "username", value: "", required: true, other props },
5
{ type: "password", name: "password", value: "", required: true, other props },
6
etc
7
]
8
9
export default fields;
10
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
:
JavaScript
1
13
13
1
handleSubmit = (e: FormEvent<HTMLFormElement>) => {
2
e.preventDefault();
3
4
const { validatedFields, errors } = fieldValidator(this.state.fields);
5
6
if(errors) {
7
this.setState({ errors });
8
return;
9
}
10
11
etc
12
}
13
but this reusable validate function has a ts error:
JavaScript
1
48
48
1
import isEmpty from "lodash.isempty";
2
3
/**
4
* Helper function to validate form fields.
5
*
6
* @function
7
* @param {array} fields - an array containing form fields.
8
* @returns {object} validated/updated fields and number of errors.
9
* @throws {error}
10
*/
11
12
const fieldValidator = <
13
T extends Array<{ type: string; value: string; required?: boolean }>
14
>(
15
fields: T,
16
): { validatedFields: T; errors: number } => {
17
try {
18
if (isEmpty(fields)) throw new Error("You must supply an array of form fields to validate!");
19
let errorCount: number = 0;
20
21
// this turns the "validatedFields" array into an { errors: string; type: string; name:
22
// string; value: string; required?: boolean | undefined;}[] type, when it needs to be "T",
23
// but "T" won't be inferred as an Array with Object props: type, value, required, value defined within it
24
const validatedFields = fields.map(field => {
25
let errors = "";
26
const { type, value, required } = field;
27
if ((!value && required) || (isEmpty(value) && required)) {
28
errors = "Required.";
29
} else if (
30
type === "email" &&
31
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(field.value)
32
) {
33
errors = "Invalid email.";
34
}
35
36
if (errors) errorCount += 1;
37
38
return { field, errors };
39
});
40
41
return { validatedFields, errors: errorCount }; // TS error here
42
} catch (err) {
43
throw String(err);
44
}
45
};
46
47
export default fieldValidator;
48
Since validatedFields
turns into:
JavaScript
1
8
1
{
2
errors: string;
3
name: string;
4
value: string;
5
type: string;
6
required: boolean;
7
}[]
8
and it’s returned { validatedFields, errors }
, it throws this TS error:
JavaScript
1
4
1
Type '{ errors: string; type: string; value: string; required?: boolean | undefined; }[]' is not assignable to type 'T'.
2
'{ 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)
3
index.ts(17, 6): The expected type comes from property 'validatedFields' which is declared here on type '{ validatedFields: T; errors: number; }'
4
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:
JavaScript
1
32
32
1
const fieldValidator = <
2
T extends any[]
3
>(
4
fields: T,
5
): { validatedFields: T; errors: number } => {
6
try {
7
if (!fields || fields.length < 0) throw new Error("You must supply an array of fields!");
8
let errorCount: number = 0;
9
10
const validatedFields = fields.map(field => {
11
let errors = "";
12
const { type, value, required }: Pick<BaseFieldProps, "type" | "value" | "required"> = field;
13
if ((!value && required) || ((value && value.length < 0) && required)) {
14
errors = "Required.";
15
} else if (
16
type === "email" &&
17
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i.test(value)
18
) {
19
errors = "Invalid email.";
20
}
21
22
if (errors) errorCount += 1;
23
24
return { field, errors };
25
}) as T;
26
27
return { validatedFields, errors: errorCount };
28
} catch (err) {
29
throw String(err);
30
}
31
};
32