TypeScript ファーストの型検証ライブラリ「Zod」とは？

# はじめに

Next.jsのチューリアル第12章をやっているときに、`Zod`というライブラリを使っていました。

何のことか分からなかったので調べることにしました。

https://nextjs.org/learn/dashboard-app/mutating-data

# Zodとは

https://zod.dev/

`TypeScript`における型の検証を簡単にしてくれるライブラリです。

---

以下のコードはNext.jsチュートリアルの第12章で実際に記述されているコードです。

この記事ではこのコードをもとに`Zod`を学びます。

```diff_tsx:/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  const rawFormData = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}
```

# 使用するコードの説明

上のコードの意味が分かってないと頭に入ってこないと思うので最初にそこを説明します。

`createInvoice`という関数は新しいデータ(請求書)を作成しようとしている関数です。

このコードでは、フォームから受け取った値を`rawFormData`に格納しているだけです。

実際のフォームが以下の画像です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3582046/06a75ab6-425d-6998-28af-85598f152ea2.png)

`<form />`タグ内には以下の要素があります。

- nameに`customerId`を持った`<select />`タグ
- nameに`amout`を持った`<input />`タグ
- nameに`status`を持った`<input />`タグ

以下コンソールに`rawFormData`を出力した場合の例

```shell:コンソール
{
  customerId: '13d07535-c59e-4157-a011-f8d2ef4e0cbb',
  amount: 234,
  status: 'pending'
}
```

# Zodを使って型検証をしよう

それでは`Zod`の説明を始めましょう。

まずインストールですね。

```shell:ターミナル
npm install zod       # npm
yarn add zod          # yarn
bun add zod           # bun
pnpm add zod          # pnpm
```

そして、見やすいようにもう一度使用するコードを貼っておきます。

```diff_tsx:/app/lib/actions.ts
'use server';
 
import { z } from 'zod';
 
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
  status: z.enum(['pending', 'paid']),
  date: z.string(),
});
 
const CreateInvoice = FormSchema.omit({ id: true, date: true });

export async function createInvoice(formData: FormData) {
  const rawFormData = CreateInvoice.parse({
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}
```

## import

`{ z }`として`import`するみたいですね。

１文字だけで`import`するという仕方で違和感を覚えました。

```tsx
import { z } from 'zod';
```

## スキーマの宣言

例えば`string`型のスキーマを宣言したい場合は、

```tsx
const TestSchema = z.string()
```

とするみたいですね。

### object

今回は受け取ったフォームの情報を`rawFormData`というオブジェクトに変換するので`z.object`を付けます。

```tsx
const FormSchema = z.object({});
```

### string

`id`と`customerId`と`date`は`string`として扱うので`z.string()`を付けます。

```diff_tsx
const FormSchema = z.object({
+ id: z.string(),
+ customerId: z.string(),


+ date: z.string(),
});
```

### number

`amount`は`number`として扱うので`z.number()`を付けます。

気になっているであろう`coerce`は下で解説します。

```diff_tsx
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
+ amount: z.coerce.number(),

  date: z.string(),
});
```

### coerce

`coerce`ではデータの変換をすることができます。

フォームから送信されるデータは、基本的にはすべて文字列として扱われます。

ですが、`amount`は`number`として扱いたいので、`coerce.number`を付けて変換しているというわけですね。

```diff_tsx
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
+ amount: z.coerce.number(),

  date: z.string(),
});
```

### enum

`status`は`pending`または`paid`という文字列のみ許可するようにしたいです。

その時には、`z.enum`を使うことで、列挙した文字列のうちいずれかの値であれば許可をすることができます。

`Typescript`でいう`"foo" | "baz"`みたいな感じですね。

```diff_tsx
const FormSchema = z.object({
  id: z.string(),
  customerId: z.string(),
  amount: z.coerce.number(),
+ status: z.enum(['pending', 'paid']),
  date: z.string(),
});
```

### omit

`omit`を使用すると宣言したスキーマから一部を削除することができます。

この場合、`FormSchema`の`id`と`date`は必要ないので、それぞれを`true`とすることで削除ができるようです。

```tsx
const CreateInvoice = FormSchema.omit({ id: true, date: true });
```

# スキーマを使って検証 parse

完成したスキーマ`CreateInvoice`を使って検証しましょう。

`スキーマ.parse(検証したい値)`とすることで検証ができます。

```tsx
export async function createInvoice(formData: FormData) {
  const rawFormData = CreateInvoice.parse({  // parse
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  });
}
```

検証が`true`だった場合は問題なく`rawFormData`にオブジェクトが代入されます。

検証が`false`だった場合はエラーになります。

# おわりに

おそらくエラーハンドリングもするべきなんでしょうけど、Nextチュートリアルの第13章で触れるみたいなのでその後追記します。

# 参考

https://qiita.com/kjkj_ongr/items/0eff5173b6e4fce7fbe8#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E%E3%81%8B%E3%82%89%E5%9E%8B%E5%AE%9A%E7%BE%A9%E3%81%99%E3%82%8B

https://zenn.dev/ynakamura/articles/65d58863563fbc