My favorite tools to make my life easier as a front-end engineer are TypeScript and Yup. As a junior developer, being able to prevent buggy code and find the source of a bug faster feels like hitting the jackpot 🏆.
I have already written an article about everything you need to know if you're getting started with TypeScript, so today I’m going to focus on Yup, a JavaScript schema builder for value parsing and validation.
I use Yup especially to validate API responses in my React and React Native projects. This schema builder helps us model complex validations and it is fast enough for runtime usage.
To better understand what I’m talking about let’s code some examples together!
Let’s say I have an API that returns an object of type DoctorsAppointment. To avoid unnecessary bugs, I want to validate the API response on the client side every time I make the call to that API. To start with, here is how my object should look like:
type DoctorsAppointment = {
id: number;
doctor: Doctor;
reasonForAppointment: string;
callType?: 'video'| 'phone';
status?: 'confirmed' | 'attended' | 'missed' | 'canceled';
doctorsNotesForPatient?: string | null;
startDate: Date;
};
interface Doctor {
name: string;
createdAt: Date;
specialities: string;
description?: string | null;
}
As you can see, some of the keys are optional (the ones with ‘?’ at the end), some of them can have the value null (doctorsNotesForPatient) while others have an object as value (doctor) or a particular value from several given options (status).
In order to make our code easier to read, we could rewrite our type like this:
const APPOINTMENT_CALL_TYPE = ['video', 'phone'] as const;
const APPOINTMENT_STATUS = ['confirmed', 'attended', 'missed', 'canceled'] as const;
type AppointmentCallType = typeof APPOINTMENT_CALL_TYPE[number];
type AppointmentStatusType = typeof APPOINTMENT_STATUS[number];
type DoctorsAppointment = {
id: number;
doctor: Doctor;
reasonForAppointment: string;
callType?: AppointmentCallType;
status?: AppointmentStatusType;
doctorsNotesForPatient?: string | null;
startDate: Date;
};
Now that we have a type for our object, let's build a schema for it!
import * as yup from 'yup';
export const DoctorsAppointmentSchema = yup.object().shape({
id: yup.number().required(),
doctor: DoctorSchema.required(),
reasonForAppointment: yup.string().required(),
callType: yup
.mixed()
.oneOf([...APPOINTMENT_CALL_TYPE])
.optional(),
status: yup
.mixed()
.oneOf([...APPOINTMENT_STATUS])
.optional(),
doctorsNotesForPatient: yup.string().optional().nullable(),
startDate: yup.date().required(),
});
;
To be less verbose, I have created a helper function, yupUtils, that will be used in all our schemas:
import * as yup from 'yup';
import { ObjectSchema } from 'yup';
const yupUtils = {
stringRequired: yup.string().required(),
stringRequiredOneOf: (arrayEnum: readonly string[]) =>
yup
.string()
.oneOf([...arrayEnum])
.required(),
stringOptionalNullableOneOf: (arrayEnum: readonly string[]) =>
yup
.mixed()
.oneOf([...arrayEnum, null])
.optional(),
stringOptionalNullable: yup.string().optional().nullable(),
dateRequired: yup.date().required(),
dateOptionalNullable: yup.date().optional().nullable(),
arrayOptionalNullableOf: (schema: ObjectSchema<any>) => yup.array().of(schema).optional().nullable(),
arrayRequiredOf: (schema: ObjectSchema<any>) => yup.array().of(schema).required(),
numberRequired: yup.number().required(),
numberOptionalNullable: yup.number().optional().nullable(),
};
After we declare our helper function, we can rewrite our schema like this:
export const DoctorsAppointmentSchema = yup.object().shape({
id: yupUtils.numberRequired,
doctor: DoctorSchema.required(),
reasonForAppointment: yupUtils.stringRequired(),
callType: yupUtils.stringOptionalNullableOneOf([...APPOINTMENT_CALL_TYPE]),
status: yupUtils.stringOptionalNullableOneOf([...APPOINTMENT_STATUS])
doctorsNotesForPatient: yupUtils.stringOptionalNullable,
startDate: yupUtils.dateRequired,
});
Much shorter and easier to read, right? 💯 Yes, that's what I thought.
If you have followed DoctorsAppointmentSchema line by line, then you have probably noticed that we don’t have a DoctorSchema yet. But don’t sweat it! All we have to do is to take a look at the Doctor interface and follow the same pattern we have used for DoctorsAppointmentSchema .
So if our TypeScript interface look like this:
interface Doctor {
name: string;
createdAt: Date;
specialities: string;
description?: string | null;
}
Then this will be our DoctorSchema:
export const DoctorsSchema = yup.object().shape({
name: yupUtils.stringRequired,
createdAt: yupUtils.dateRequired
specialities:yupUtils.stringRequired,
description: yupUtils.stringOptionalNullable
});
In order to validate the schemas we get from the API, we can create another helper function.
import * as yup from 'yup';
import { ObjectSchema } from 'yup';
import { MixedSchema } from 'yup/es/mixed';
import { AnyObject } from 'yup/es/types';
export const isValidYupSchema = async (
schema: ObjectSchema<any> | MixedSchema<any> | AnyObject,
value: any,
): Promise<boolean> => {
try {
return !!(await schema?.validate(value, { strict: true }));
} catch (error) {
console.log('Error at validating schema:', error);
}
return false;
};
Once we have created the helper function, we can use it to validate all our schemas. Below is an example of how we can leverage isValidYupSchema to validate our DoctorsAppointment.
export const parseDoctorsAppointment = async (object: any): Promise<DoctorsAppointment | undefined> => {
try {
if (await isValidYupSchema(DoctorsAppointment, object)) {
return object as DoctorsAppointment;
}
console.log('Schema is not valid');
return undefined;
} catch (error) {
return error;
}
};
Hopefully this real-life example on how to validate an API’s response has been useful to you. If not, I’ll gladly answer any question related to Yup implementation in React projects 🚀.