[Typescript] Using Zod to do runtime type checking

Zod

Example: number:

import { expect, it } from "vitest";
import { z } from "zod";

const numberParser = z.number();

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

// TESTS

it("Should throw a runtime error when called with not a number", () => {
  expect(() => toString("123")).toThrowError(
    "Expected number, received string",
  );
});

it("Should return a string when called with a number", () => {
  expect(toString(1)).toBeTypeOf("string");
});

 

Example Object:

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

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

  console.log(parsedData) // the parseData will reudce to the type its understand,: {name: 'C-3PO'}

  return parsedData.name;
};

// TESTS

it("Should return the name", async () => {
  expect(await fetchStarWarsPersonName("1")).toEqual("Luke Skywalker");
  expect(await fetchStarWarsPersonName("2")).toEqual("C-3PO");
});

 

Example Array:

import { expect, it } from 'vitest';
import { z } from 'zod';

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

const StarWarsPeopleResults = z.object({
  results: z.array(StarWarsPerson),
});
//                            ^ 🕵️‍♂️

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

// TESTS

it('Should return the name', async () => {
  expect((await fetchStarWarsPeople())[0]).toEqual({
    name: 'Luke Skywalker',
  });
});

 

Example: z.infer<typeof object>

import { z } from 'zod';

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

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

export type PeopleResults = z.infer<typeof StarWarsPeopleResults>;
const logStarWarsPeopleResults = (
  data: PeopleResults
) => {
  //                                    ^ 🕵️‍♂️
  data.results.map((person) => {
    console.log(person.name);
  });
};

 

Example: Optional:

import { z } from 'zod';

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

type FormType = z.infer<typeof Form>;

/*
type FormType = {
  phoneNumber?: string | undefined;
  name: string
}
*/

 

Example: default value:

import { expect, it } from 'vitest';
import { z } from 'zod';

const Form = z.object({
  repoName: z.string(),
  keywords: z.array(z.string()).default([]),   // set default value
});

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

  return parsedData;
};

// TESTS

it('Should include keywords if passed', async () => {
  const result = validateFormInput({
    repoName: 'mattpocock',
    keywords: ['123'],
  });

  expect(result.keywords).toEqual(['123']);
});

it('Should automatically add keywords if none are passed', async () => {
  const result = validateFormInput({
    repoName: 'mattpocock',
  });

  expect(result.keywords).toEqual([]);
});

 

 

The Input is Different than the Output

We've reached the point with Zod where our input is different than our output.

In other words, you can generate types based on input as well as generating types based on the output.

For example, let's create FormInput and FormOutput types:

type FormInput = z.input<typeof Form>
/*
type FormInput = {
  keywords?: string[] | undefined;
  repoName: string;
}
*/
type FormOutput = z.infer<typeof Form>
/*
type FormOutput = {
  repoName: string;
  keywords: string[];
}
*/

 

Example Union type:

 


import { expect, it } from "vitest";
import { z } from "zod";

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

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

  return parsedData;
};

// TESTS

it("Should fail if an invalid privacyLevel passed", async () => {
  expect(() =>
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "something-not-allowed",
    }),
  ).toThrowError();
});

it("Should permit valid privacy levels", async () => {
  expect(
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "private",
    }).privacyLevel,
  ).toEqual("private");

  expect(
    validateFormInput({
      repoName: "mattpocock",
      privacyLevel: "public",
    }).privacyLevel,
  ).toEqual("public");
});

 

Example: string-specific validations

z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().email();
z.string().url();
z.string().uuid();
z.string().cuid();
z.string().regex(regex);
z.string().startsWith(string);
z.string().endsWith(string);

// trim whitespace
z.string().trim();

// deprecated, equivalent to .min(1)
z.string().nonempty();

// optional custom error message
z.string().nonempty({ message: "Can't be empty" });
import { expect, it } from "vitest";
import { z } from "zod";

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

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

  return parsedData;
};

// TESTS

it("Should fail if you pass a phone number with too few characters", async () => {
  expect(() =>
    validateFormInput({
      name: "Matt",
      email: "matt@example.com",
      phoneNumber: "1",
    }),
  ).toThrowError("String must contain at least 5 character(s)");
});

it("Should fail if you pass a phone number with too many characters", async () => {
  expect(() =>
    validateFormInput({
      name: "Matt",
      email: "matt@example.com",
      phoneNumber: "1238712387612387612837612873612387162387",
    }),
  ).toThrowError("String must contain at most 20 character(s)");
});

it("Should throw when you pass an invalid email", async () => {
  expect(() =>
    validateFormInput({
      name: "Matt",
      email: "matt",
    }),
  ).toThrowError("Invalid email");
});

it("Should throw when you pass an invalid website URL", async () => {
  expect(() =>
    validateFormInput({
      name: "Matt",
      email: "matt@example.com",
      website: "/",
    }),
  ).toThrowError("Invalid url");
});

it("Should pass when you pass a valid website URL", async () => {
  expect(() =>
    validateFormInput({
      name: "Matt",
      email: "matt@example.com",
      website: "https://mattpocock.com",
    }),
  ).not.toThrowError();
});

 

Example:  Composing Schemas

// From 
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(),
});
// Notice that id is present in each.
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()
})
const ObjectWithId = z.object({
    id: z.string().uuid()
})

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

const Post = ObjectWithId.merge(
  z.object({
    title: z.string(),
    body: z.string()
  })
)

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

 

Transform Data from Within a Schema

Another useful feature of Zod is manipulating data from an API response after parsing.

To see this in action, we're going back to our Star Wars example.

Recall that we created StarWarsPeopleResults with a results array of StarWarsPerson schemas.

When we get the name of a StarWarsPerson from the API, it's their full name.

What we want to do is add a transformation on StarWarsPerson itself

import { expect, it } from "vitest";
import { z } from "zod";

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

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

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

// TESTS

it("Should resolve the name and nameAsArray", async () => {
  expect((await fetchStarWarsPeople())[0]).toEqual({
    name: "Luke Skywalker",
    nameAsArray: ["Luke", "Skywalker"],
  });
});

 

posted @   Zhentiw  阅读(120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2020-10-30 [Kotlin] Try resource, use {} block
2020-10-30 [Kotlin] Try catch & Throw
2020-10-30 [Kotlin] mutableList with toList
2018-10-30 [React] Use React.memo with a Function Component to get PureComponent Behavior
2018-10-30 [Javascript] Use a custom sort function on an Array in Javascript
2017-10-30 [Anuglar & NgRx] StoreRouterConnectingModule
2017-10-30 [Unit testing] data-test attr FTW
点击右上角即可分享
微信分享提示