【TypeScript】Zod でスキーマ宣言とバリデーションを実施する
[Zod](https://github.com/colinhacks/zod) は TypeScript first でスキーマ宣言とバリデーションを実施するためのライブラリです。 一度バリデータを宣言すれば、Zod が自動的に TypeScript の型を推論してくれるという特徴があります。このおかげで重複した型宣言を排除できます。 また、Zod はエコシステムも多く存在しており、OpenApi、Nest.js、Prisma、react-hook-form などと組み合わせて使うことができます。
Zod は TypeScript first でスキーマ宣言とバリデーションを実施するためのライブラリです。
一度バリデータを宣言すれば、Zod が自動的に TypeScript の型を推論してくれるという特徴があります。このおかげで重複した型宣言を排除できます。
また Zod はエコシステムも多く存在しており、OpenApi、Nest.js、Prisma、react-hook-form などと組み合わせて使うことができます。
https://github.com/colinhacks/zod#ecosystem
簡単な使い方
まずは 1 番簡単な使い方から見ていきましょう。Zod は TypeScript 4.1 以上が必要です。また strict
モードを有効にする必要があります。
// tsconfig.json
{
// ...
"compilerOptions": {
// ...
"strict": true
}
}
以下のコマンドで Zod をインストールします。
npm install zod # npm
yarn add zod # yarn
pnpm add zod # pnpm
1 番簡単な string
型のスキーマを定義します。
import { z, ZodError } from 'zod' // ①
const usernameSchema = z.string() // ②
type Username = z.infer<typeof usernameSchema> // ③
// type Username = string
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest
it('should work', () => {
expect(usernameSchema.parse('john')).toBe('john') // ④
})
it('should fail', () => {
expect(() => usernameSchema.parse(1)).toThrow(ZodError) // ⑤
})
}
① zod
モジュールから z
をインポートします。z
はスキーマを定義するための関数を持っています。
② z.string()
関数でスキーマを宣言します。z.string()
は名前のとおり string
型のスキーマを作成します。
③ z.infer
の型引数に宣言したスキーマを渡すことでスキーマから TypeScript の型を生成できます。usernameSchema
は string
型のスキーマなので string
型に推論されます。
④ 定義したスキーマの使い方をテストしています。usernameSchema.parse()
関数を使うことで値を検証できます。ここでは 'john'
という string
型の値を渡しているのでバリデーションをパスしてそのまま値が返されます。
⑤ 続いて usernameSchema.parse()
関数に 1
を渡しています。usernameSchema
には string
型が期待されていますが、実際には number
型が渡されているため ZodError
が例外として投げられます。
また usernameSchema.scheme()
で返される値の型は string
型となるので unkown
型を安全に使用できます。
const toUpper = (value: unknown): stirng => {
try {
//
const username = usernameSchema.parse(value)
// const username: string
return username.toUpperCase()
} catch (e) {
if (e instanceof ZodError) {
console.log(e.message)
}
throw e
}
}
詳細なバリデーション
それぞれのプリミティブ型に合わせて、もう少し詳細なバリデーションを定義できます。例えば z.string().max()
や z.string().min()
は文字列の長さを検証したり、z.number.positive()
は 0
より大きい値であることを検証します。
また { message: '...' }
オプションを引数に渡すことで、エラーメッセージをカスタマイズできます。
import { z, ZodError } from 'zod'
const usernameSchema = z.string()
.min(3, { message: 'Username must be at least 3 characters' })
.max(20, { message: 'Username must be at most 20 characters' })
const ageSchema = z.number()
.nonnegative({ message: 'Age must be non-negative' })
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest
test('invalid username', () => {
expect(() => usernameSchema.parse('hi')).toThrow('Username must be at least 3 characters')
expect(() => usernameSchema.parse('a'.repeat(21))).throws('Username must be at most 20 characters')
})
test('valid username', () => {
expect(usernameSchema.parse('hello')).toEqual('hello')
})
test('invalid age', () => {
expect(() => ageSchema.parse(-1)).toThrow('Age must be non-negative')
})
test('valid age', () => {
expect(ageSchema.parse(1)).toEqual(1)
})
}
API のレスポンスの値を検証する
もう少し実践的な例を見ていきましょう。普段 fetch API
や axios
など HTTP クライアントを使用するときは API が返すレスポンスを信頼して型を付けることが多いかと思います。ただし、実際にスキーマどおりのレスポンスが返却されるかどうかはランタイムになってみないとわかりません。期待されたスキーマのレスポンスが返ってこないことを考えるとゾッとしますよね・1
API のレスポンスのスキーマを Zod
で作成し検証も実行できるようにしてみましょう。z.object
でオブジェクト型のスキーマを宣言できます。
import { z, ZodError } from 'zod'
const TodoSchema = z.object({
id: z.number(),
userId: z.number(),
title: z.string(),
completed: z.boolean(),
})
type Todo = z.infer<typeof TodoSchema>
// type Todo = {
// id: number;
// userId: number;
// title: string;
// completed: boolean;
// }
const TodoResponseSchema = z.array(TodoSchema)
type TodoResponse = z.infer<typeof TodoResponseSchema>
// type TodoResponse = {
// id: number;
// userId: number;
// title: string;
// completed: boolean;
// }[]
const fetchTodos = async (): Promise<TodoResponse> => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`)
}
const json = await response.json()
// const json: any
const todos = TodoResponseSchema.parse(json)
// const todos: {
// id: number;
// userId: number;
// title: string;
// completed: boolean;
// }[]
return todos
} catch (e) {
if (e instanceof ZodError) {
// handlel validation error
}
throw e
}
}
fetch
のレスポンスにバリデーションを実施したうえで型を付けることができるので、res.json() as TodoResponse
のように API を信頼して型を付けるよりはるかに安全に使用できます。
また TypeScript の型を Zod のスキーマから生成できるので、重複したコードを書いたりバリデーションをと型情報の整合性が取れないといったことが発生することがありません。
Footnotes
-
Zod だけにね ↩