zod を実際のプロジェクトで使う時にどうするか考えた

# [zod](https://github.com/colinhacks/zod)
なんか最近流行っているらしいスキーマの定義とバリデーションのライブラリ。

基本はこんな感じ。

```typescript
import { z, ZodError } from "zod";

// スキーマを定義する
const schema = z.object({
  id: z.string().max(3),
  name: z.string().nonempty(),
})

// バリデーションを行なう
try {
  // バリデーションが成功したときは user にオブジェクトが代入される
  const user = schema.parse({
    id: "abcde",
    name: "shiraishi"
  })
  
} catch (error) {
  console.log(error); 
}

// スキーマから型を生成する
type User = z.infer<typeof schema>
// {
//   id: string,
//   name: string,
// }
```

バリデーションが失敗した場合はこんなに親切な `ZodError` が throw されます。

```
ZodError: [
  {
    "code": "too_big",
    "maximum": 3,
    "type": "string",
    "inclusive": true,
    "message": "Should be at most 3 characters long",
    "path": [
      "id"
    ]
  }
]
```

他にもいろいろなメッソドや型が利用可能です。[公式ドキュメント](https://github.com/colinhacks/zod)に全て記載されています。

# 実際のプロジェクトでどう使う？？

[公式ドキュメント](https://github.com/colinhacks/zod)はとても詳しく書いてありますが、実際のプロジェクトでどんな感じで使うのかまでは書いていません。zod を紹介する Qiita や zenn の記事でも、言及するものが少なかったので簡単に書いてみます。

今の所ぱっと思いつくのは

1. schema をそのまま使うパターン
2. class でラップするパターン

です。

## schema をそのまま使うパターン
### ディレクトリ構成
例えば、`Vue.js` などで使う場合は下記のようなディレクトリ構成になります。

```
src
├── components
├── pages
├── schemas
│   ├── user_form_schema.ts
│   └── user_schema.ts
:
```
### userSchema
`userSchema` は API の**レスポンス**などから受け取った値を `User` オブジェクトに変換し、そのスキーマを保証するためのものです。

```ts:user_schema.ts
import { z, ZodError } from "zod";

export const userSchema = z.object({
  id: z.string().max(3),
  name: z.string().nonempty(),
});

export type User = z.infer<typeof userSchema>;
```

`userSchema`を利用する側は次の様になります。

```ts
async function getUser(id: string) {
  const data = await api.get(`/users/${id}`);
  return userSchema.parse(data);
}
```

### userFormSchema
`user_form_schema` は API **リクエスト**を送る直前などにバリデーションチェックをするためのものです。

```ts:user_form_schema.ts
import { z, ZodError } from "zod";

export const userFormSchema = z.object({
  name: z.string().nonempty(),
});

export type UserForm = z.infer<typeof userFormSchema>;
```

`userFormSchema` を利用する側は次の様になります。(`Vue.js` の `methods` っぽく書いています。)

```ts
async function onSubmit(userForm: UserForm) {
  try {
    api.putUser(userFormSchema.parse(userForm));

  } catch (error) {
    if(error instanceof ZodError) {
      this.errorMessage = error.issues[0].message;
    };
  };
};
```

### メリット
- zod のインターフェースをフル活用できる
- シンプル

### デメリット
- `User`にメソッドやゲッターを持たせることができない

## class でラップするパターン
### ディレクトリ構成
前述の例と同様に`Vue.js` などで使う場合は下記のようなディレクトリ構成になります。

```
src
├── components
├── forms
│   └── user_form.ts
├── models
│   └── user.ts
├── pages
:
```

### User クラス
`User` は API の**レスポンス**などから受け取ったユーザのモデルクラスです。

```ts
import { z } from "zod";

export const schema = z.object({
  id: z.string().max(3),
  firstName: z.string().nonempty().max(10),
  lastName: z.string().nonempty().max(10),
});

type Schema = z.infer<typeof schema>

export class User {
  constructor(data: any) {
    this.value = schema.parse(data);
  };

  value!: Schema;

  get fullName() {
    return `${this.value.firstName} ${this.value.lastName}`
  }
};
```

`User`を利用する側は次の様になります。

```ts
async function getUser(id: string) {
  const data = await api.get(`/users/${id}`);
  return new User(data);
}

// 各パラメータへのアクセス
console.log(user.value.id)
```

### UserForm
`UserForm` は API **リクエスト**のパラメータを保持し、バリデーションを行なうクラスです。

```ts:UserForm.ts
import { z, ZodError } from "zod";

const schema = z.object({
  firstName: z.string().nonempty().max(10),
  lastName: z.string().nonempty().max(10),
})

type Schema = z.infer<typeof schema>

export class UserForm {
  value: Partial<Schema> = {}

  private internalError?: ZodError

  get error(): ZodError {

    // 参照ではなくコピーを返す
    return _.cloneDeep(this.internalError)
  }

  validate() {
    try {
      const checkedValue = schema.parse(this.value)

    } catch (error) {
      if(error instanceof ZodError) {
        this.internalError = error
      }
    }
  }
}
```

`User`を利用する側は次の様になります。

```ts
async function onSubmit(userForm: UserForm) {
  userForm.validate();
  this.error = userForm.error();

  if (!this.error) {
    api.putUser(userFormSchema.parse(userForm));
  }
}
```

### メリット
- `User` や `UserForm` にメソッドやゲッターを持たせることができる

### デメリット
- これは本来想定されている zod の使い方なのか？と少し不安になる。

# まとめ
実際のプロジェクトを意識して zod でスキーマを書いてみました。
ちょっと雑に書いてしまいましたので、

- こんな風に書くと良いよ！
- このプロダクトはイケてる書き方してるよ！

とかあれば是非教えてほしいです。