A function getFormData()
validates and sanitises a FormData
object using some provided schema
. If there are no validation errors, the function will return the sanitised FormData
; otherwise the function will throw an error.
Problem:
The current implementation of getFormData()
returns a FormDataValues
type (i.e. Record<string, any>
); and takes a parameter of type Schema
(i.e. Record<string, ValidatorFunction<any>>
) where any
is the returned value type of the ValidatorFunction
.
This current implementation is not type-safe. How can I instead ensure type-safety by inferring the return type of the getFormData()
function from the provided schema
?
Example:
The expected implementation could take a schema
object of type { name: ValidatorFunction<string> }
and return a values
object of type { name: string }
.
// api.ts
const validateName: ValidatorFunction<string> = (name) => {
if (!name) {
return { value: name, error: "Name is required" };
}
if (typeof name !== "string") {
return { value: name, error: "Name must be a string" };
}
name = name.trim();
return { value: name, error: null };
};
const schema = { name: validateName };
const action = async ({ request }) => {
try {
/**
* Infer return type of getFormData() from schema parameter, e.g.
* const schema: { name: ValidatorFunction<string> } ->
* const values: { name: string }
*/
const values = await getFormData(request, schema);
return null;
} catch (error) {
console.error(error);
}
};
// form-data.ts
type FormDataValue = FormDataEntryValue | FormDataEntryValue[] | null;
type ValidatorFunction<T> = (value: FormDataValue) => { value: T; error: null } | { value: FormDataValue; error: string };
type Schema = Record<string, ValidatorFunction<any>>;
type FormDataValues = Record<string, any>;
type FormDataErrors = Record<string, string>;
const getFormData = async (request: Request, schema: Schema) => {
const formData = await request.formData();
const formDataValues: FormDataValues = {};
const formDataErrors: FormDataErrors = {};
for (const field in schema) {
const formDataValue = _parseFormDataValue(formData, field);
const validator = schema[field];
const { value, error } = validator(formDataValue);
if (error) {
formDataErrors[field] = error;
}
formDataValues[field] = value;
}
if (Object.keys(formDataErrors).length !== 0) {
throw formDataErrors;
}
return formDataValues;
};
const _parseFormDataValue = (formData: FormData, field: string) => {
const formDataEntryValues = formData.getAll(field);
if (formDataEntryValues.length == 1) {
return formDataEntryValues[0];
}
if (formDataEntryValues.length > 1) {
return formDataEntryValues;
}
return null;
};
CodePudding user response:
First we can use a conditional type to infer the value type from a validator function.
type ValidatorFunctionReturn<F> = F extends ValidatorFunction<infer T> ? T : F;
type ValidateNameReturn = ValidatorFunctionType<typeof validateName> // string
The we can create a mapped type that maps over a Schema
keys and uses the conditional type to infer the type for the key.
type SchemaValues<S extends Schema> = {
[P in keyof S]: ValidatorFunctionReturn<S[P]>;
}
const schema = { name: validateName };
type Foo = SchemaValues<typeof schema> // {name: string}
Then your getFormData
function would look something like this:
const getFormData = async <S extends Schema>(request: Request, schema: S) : Promise<SchemaValues<S>> => {
// ...
}