[Typescript] Using Zod to do runtime type checking
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"],
});
});
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源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