01 – Runtime Type Checking with Zod


TypeScript is a very useful type tool for checking the type of variables in code


But we can’t always guarantee the type of variables in our code, such as those coming from API interfaces or form inputs.


The Zod library enables us to check the type of a variable at   , which is useful for most of our projects!

 First exploration runtime check

 Check out this toString function:

export const toString = (num: unknow) => {
    return String(num)
}


We set the entry for num to unknow


This means that we can pass any type of parameter to the toString function during the coding process, including object types or undefined :

toString('blah')
toString(undefined)
toString({name: 'Matt'})


So far no errors have been reported, but we want to prevent this from happening at  


If we pass a string to toString , we want to throw an error and indicate that a number was expected but a string was received

it("1111", () => {
  expect(() => toString("123")).toThrowError(
    "Expected number, received string",
  );
});


If we pass in a number, toString is able to function properly

it("2222", () => {
    expect(toString(1)).toBeTypeOf("string");
});

 prescription

 Create a numberParser


The various parsers are one of the most basic features of Zod.


We use z.number() to create a numberParser


It creates the z.ZodNumber object, which provides some useful methods

const numberParser = z.number();


If the data is not of a numeric type, then passing it to numberParser.parse() will result in an error.


This means that all variables passed into numberParser.parse() will be converted to numbers before our test can pass.


Add numberParser , update toString method


const numberParser = z.number();

export const toString = (num: unknown) => {
  const parsed = numberParser.parse(num);
  return String(parsed);
};

 Try different types

 Zod also allows other type checks


For example, if the parameter we want to receive is not a number but a boolean value, then we can change numberParser to z.boolean()


Of course, if we only change this, then our original test case will report an error!


This technique in Zod provides us with a solid foundation. As we get deeper into using it, you’ll find that Zod mimics a lot of what you’re used to in TypeScript.


The full base type of Zod can be viewed here


02 – Verifying Unknown APIs with Object Schema


Zod is often used to validate unknown API returns.


In the following example, we get information about a character from the Star Wars API

export const fetchStarWarsPersonName = async (id: string) => {
  const data = await fetch("<https://swapi.dev/api/people/>" + id).then((res) =>
    res.json(),
  );

  const parsedData = PersonResult.parse(data);

  return parsedData.name;
};


Notice that the data now being processed by PersonResult.parser() is coming from fetch requests


PersonResult The variable was created by z.unknown() , which tells us that the data is considered to be of type unknown because we don’t know what’s contained in that data.

const PersonResult = z.unknown();

 operational test


If we print out the fetch function’s return value at console.log(data) , we can see that the API returns a lot of things, not just the name of the character, but also other things like eye_color, skin_color, and so on that we’re not interested in.


Next we need to fix the unknown type of this PersonResult

 prescription


Use z.object to modify PersonResult


First, we need to change PersonResult to z.object


It allows us to define these objects using a key and a type.


In this example, we need to define name to be the string

const PersonResult = z.object({
  name: z.string(),
});


Notice that this is a bit like when we create an interface in TypeScript.

interface PersonResult {
    name: string;
}

 Check our work


In fetchStarWarsPersonName , our parsedData has now been given the correct type and has a structure that Zod recognizes


Recalling the API, we can still see that the returned data contains a lot of information that we’re not interested in.


Now if we print parsedData with console.log , we can see that Zod has already filtered out the Keys we’re not interested in for us, giving us only the name field


Any additional keys added to PersonResult will be added to parsedData


The ability to explicitly specify the type of each key in the data is a very useful feature in Zod.

 03 – Creating Custom Type Arrays


In this example, we’re still using the Star Wars API, but this time we’re getting the data for the 所有 character


The beginning part is very similar to what we saw before, the StarWarsPeopleResults variable is set to the z.unknown()

const StarWarsPeopleResults = z.unknown();

export const fetchStarWarsPeople = async () => {
  const data = await fetch("https://swapi.dev/api/people/").then((res) =>
    res.json(),
  );

  const parsedData = StarWarsPeopleResults.parse(data);

  return parsedData.results;
};


Similar to before, adding console.log(data) to the fetch function, we can see that there is a lot of data in the array even though we are only interested in the name field of the array.


If this were a TypeScript interface, it would probably be written like this

interface Results {
  results: {
    name: string;
  }[];
}


Represent an array of StarWarsPerson objects by updating StarWarsPeopleResults with the object schema

 You can refer to the documentation here for help.

 prescription


The correct solution is to create an object to drink other objects. In this example, StarWarsPeopleResults would be an object containing the results attribute z.object


For results , we use z.array and provide StarWarsPerson as a parameter. We also don’t need to rewrite the name: z.string() section

 This is the previous code

const StarWarsPeopleResults = z.unknown()

 after modification

const StarWarsPeopleResults = z.object({
  results: z.array(StarWarsPerson),
});


If we console.log this parsedData , we can get the desired data


Declaring an array of objects like the above is the most common use of z.array() all the time, especially when the object has already been created.

 04 – Extracting object types


Now we use the console function to print StarWarsPeopleResults to the console.

const logStarWarsPeopleResults = (data: unknown) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};


Once again, the type of data is unknown

 To fix it, one might try to use something like the following:

const logStarWarsPeopleResults = (data: typeof StarWarsPeopleResults)


However this will still be a problem because this type represents the type of the Zod object and not the StarWarsPeopleResults type

 Update logStarWarsPeopleResults function to extract object types

 prescription

 Update this print function


Use z.infer and pass typeof StarWarsPeopleResults to fix the problem!

const logStarWarsPeopleResults = (
  data: z.infer<typeof StarWarsPeopleResults>,
) => {
  ...


Now when we hover the mouse over this variable in VSCode, we can see that its type is an object containing results


When we update the schema StarWarsPerson , the function’s data will be updated as well.


This is a great way to do type-checking at runtime using Zod, but also to get the type of the data at build time!

 An alternative program


Of course, we can also save StarWarsPeopleResultsType as a type and export it from the file

export type StarWarsPeopleResultsType = z.infer<typeof StarWarsPeopleResults>;

  logStarWarsPeopleResults function would be updated to look like this

const logStarWarsPeopleResults = (data: StarWarsPeopleResultsType) => {
  data.results.map((person) => {
    console.log(person.name);
  });
};


This way other files can also get the StarWarsPeopleResults type if needed


05 – Making schema optional

 Zod is equally useful in front-end projects


In this example, we have a function called validateFormInput


Here values is of type unknown , which is safe because we don’t know the fields of this form in particular. In this example, we have collected name and phoneNumber as the schema for the Form object.

const Form = z.object({
  name: z.string(),
  phoneNumber: z.string(),
});

export const validateFormInput = (values: unknown) => {
  const parsedData = Form.parse(values);

  return parsedData;
};


As it stands now, our test will report an error if the phoneNumber field is not committed


Because phoneNumber is not always necessary, we need to come up with a scheme that allows our test cases to pass regardless of whether phoneNumber is submitted or not

 prescription


In this case, the solution is very intuitive! Add .optional() to the phoneNumber schema and our test will pass!

const Form = z.object({ name: z.string(), phoneNumber: z.string().optional(), });


What we are saying is that the name field is a required string, phoneNumber may be a string or undefined


We don’t need to do anything extra, and making the schema optional is a very good solution!


06 – Setting Defaults in Zod


Our next example is much like the previous one: a form form input validator that supports optional values.


This time, Form has a repoName field and an optional array field keywords

const Form = z.object({
  repoName: z.string(),
  keywords: z.array(z.string()).optional(),
});


To make the actual form easier, we want to set it up so that we don’t have to pass in an array of strings.


Modify Form so that when the keywords field is empty, there is a default value (empty array)

 prescription


Zod’s default schema function, which allows a field to be supplied with a default value if it is not passed a parameter.


In this example, we will use .default([]) to set up an empty array

keywords: z.array(z.string()).optional()

keywords: z.array(z.string()).default([])


Since we added the default value, we don’t need to use optional() anymore, the optional is already included.

 After the modification, our test can be passed

 Inputs are different from outputs


In Zod, we’ve gotten to the point where the inputs are different from the outputs.


That is to say, we can do type generation based on inputs as well as outputs


For example, let’s create the types FormInput and FormOutput

type FormInput = z.infer<typeof Form>
type FormOutput = z.infer<typeof Form>

 present (sb for a job etc) z.input


As written above, the input is not exactly correct, because when we are passing parameters to validateFormInput , we don’t have to necessarily pass the keywords field


Instead of z.infer , we can use z.input to modify our FormInput.


If there is a discrepancy between the input and output of the validation function, it provides us with an alternative way of generating the type.

type FormInput = z.input<typeof Form>

 07 – Clarification of permissible types

 In this example, we will once again validate the form


This time, the Form form has a privacyLevel field, which only allows the two types private or public .

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.string(),
});


If we were in TypeScript, we would write it like this

type PrivacyLevel = 'private' | 'public'


Of course, we could use the boolean type here, but if we need to add new types to PrivacyLevel in the future, that would not be appropriate. It’s safer to use a union or enum type here.


The first test reports an error because our validateFormInput function has values other than “private” or “public” passed into the PrivacyLevel field.

it("2222", async () => {
  expect(() =>
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "something-not-allowed",
    }),
  ).toThrowError();
});


Your task is to find a Zod API that allows us to specify the string type of the incoming parameter as a way to get the test to pass.

 prescription


Unions & Literals


For the first solution, we’ll use Zod’s union function and pass an array of “private” and “public” literals.

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.union([z.literal("private"), z.literal("public")]),
});


Literals can be used to represent: numbers, strings, boolean types; they cannot be used to represent object types.


We can use z.infer to check the type of our Form

type FormType = z.infer<typeof Form>


In VS Code if you mouse over the FormType, we can see that privacyLevel has two optional values: “private” and “public”.

 What could be considered a more concise solution: enumeration


The same thing can be done by using the Zod enumeration via z.enum as follows:

const Form = z.object({
  repoName: z.string(),
  privacyLevel: z.enum(["private", "public"]),
});


Instead of using syntactic sugar to parse literals, we can use the z.literal


This approach does not produce enumerated types in TypeScript such as

enum PrivacyLevcel {
    private,
    public
}

 A new union type is created


Similarly, we can see a new union type containing “private” and “public” by hovering over the type


08 – Complex schema validation


So far, our form validator function has been able to check a variety of values


The form has a name, email field and optional phoneNumber and website fields.

 However, we now want to make strong constraints on some values


Need to restrict users from entering illegitimate URLs and phone numbers


Your task is to find Zod’s API to do the validation for the form type


The phone number needs to be in the right characters, and the email address and URL need to be formatted correctly.

 prescription


The strings section of the Zod documentation contains some examples of checksums that can help us pass the test with flying colors.


Now our Form form schema would look like this

const Form = z.object({
  name: z.string().min(1),
  phoneNumber: z.string().min(5).max(20).optional(),
  email: z.string().email(),
  website: z.string().url().optional(),
});


name field is added to min(1) because we can’t pass an empty string to it


phoneNumber limits the string length to 5 to 20, and it is optional.


Zod has built-in mailbox and url validators, so we don’t need to manually write these rules ourselves.


You can notice that we can’t write .optional().min() this way because the optional type doesn’t have a min attribute. This means that we need to write .optional() after each checker


There are many other checker rules that we can find in the Zod documentation


09 – Reducing Duplication by Combining Schemas

 Now, let’s do something different.


In this example, we need to find solutions to refactor the project to reduce duplicate code


Here we have these schemas, including: User , Post and Comment

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});


We see that the id is present in every schema.


Zod provides a number of options for organizing object objects into different types, allowing us to make our code more compliant with the DRY principle


Your challenge is that you need to refactor the code using Zod to reduce id rewriting

 About Test Case Syntax


You don’t have to worry about the TypeScript syntax of this test case, here’s a quick explanation:

Expect<
  Equal<z.infer<typeof Comment>, { id: string; text: string }>
>


In the code above, Equal is confirming that z.infer<typeof Comment> and {id: string; text: string} are of the same type


If you remove the id field from Comment , then you can see in VS Code that Expect will have an error reported because the comparison doesn’t hold up anymore

 prescription

 There are many ways we can refactor this code

 For reference, here’s what we started with:

const User = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

const Post = z.object({
  id: z.string().uuid(),
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: z.string().uuid(),
  text: z.string(),
});

 Simple program


The simplest solution would be to extract the id field and save it as a separate type, which could then be referenced by every z.object

const Id = z.string().uuid();

const User = z.object({
  id: Id,
  name: z.string(),
});

const Post = z.object({
  id: Id,
  title: z.string(),
  body: z.string(),
});

const Comment = z.object({
  id: Id,
  text: z.string(),
});


That’s a pretty good solution, but the id: ID segment still keeps repeating itself. All the tests pass, so that’s okay too

 Using the Extend method


Another option is to create a base object called ObjectWithId that contains the id field

const ObjectWithId = z.object({
  id: z.string().uuid(),
});


We can use the extension method to create a new schema to add the base object

const ObjectWithId = z.object({
  id: z.string().uuid(),
});

const User = ObjectWithId.extend({
  name: z.string(),
});

const Post = ObjectWithId.extend({
  title: z.string(),
  body: z.string(),
});

const Comment = ObjectWithId.extend({
  text: z.string(),
});

 Note that .extend() overrides the field

 Using the Merge method


Similar to the above scenario, we can use the merge method to extend the base object ObjectWithId :

const User = ObjectWithId.merge(
  z.object({
    name: z.string(),
  }),
);


Using .merge() would be more verbose than .extend() . We must pass a z.object() object containing z.string()


Merging is usually used to unite two different types, not just to extend a single type


These are a few different ways to group objects together in Zod to reduce the amount of code duplication, make the code more DRY compliant, and make the project easier to maintain!


10 – Converting data by schema

 

 prescription


As a reminder, this is what StarWarsPerson looked like before the conversion:

const StarWarsPerson = z.object({
  name: z.string()
});

 Add a Transformation


When we have the name field in .object() , we can get the person parameter and then convert it and add it to a new property

const StarWarsPerson = z
  .object({
    name: z.string(),
  })
  .transform((person) => ({
    ...person,
    nameAsArray: person.name.split(" "),
  }));


Inside .transform() , person is the object containing name above.


This is also where we add the nameAsArray attribute that satisfies the test.


All of this happens in the StarWarsPerson scope, not inside the fetch function or elsewhere.

 another example


Zod’s conversion API applies to any of its primitive types.


For example, we can convert name inside of z.object

const StarWarsPerson = z
  .object({
    name: z.string().transform((name) => `Awesome ${name}`)
  }),
  ...


Now we have a name field containing the Awesome Luke Skywalker and a nameAsArray field containing the ['Awesome', 'Luke', 'Skywalker']

 The conversion process works at the bottom level, can be combined and is very useful

By lzz

Leave a Reply

Your email address will not be published. Required fields are marked *