zodでfirestoreのTimestamp型を取り扱うスキーマを作る

# 動機

[zod]でFirestoreに格納するオブジェクトのスキーマを作るとき、Timestamp型のプロパティに指定するためのスキーマ定義が欲しい。

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

/** ユーザー情報スキーマ */
const userSchema = z.object({
  /** ユーザー名 */
  name: z.string().max(100).default(""),

  /** 最終ログイン日時 */
  lastLoginedAt: // 【ここに指定するスキーマは？】 
});
```

# スキーマ定義

```typescript:firebaseSchema.ts
import { z } from "zod";
import { FieldValue, Timestamp } from "firebase/firestore";

/** firestoreのFieldValueオブジェクトを受け付けるスキーマ */
export const firestoreFieldValueSchema = z.custom<FieldValue>(
  (data) => data instanceof FieldValue
);

/** firestoreのTimestampオブジェクトを受け付けるスキーマ（FieldValueは受け付けない） */
export const firestoreTimestampSchema = z.instanceof(Timestamp);

/** firestoreのTimestampオブジェクトまたはFieldValueオブジェクトを受け付けるスキーマ */
export const firestoreTimestampLooseSchema = z.union([
  firestoreFieldValueSchema,
  firestoreTimestampSchema,
]);
```

※Firebase v9以降を前提としています（v8以前はインターフェースが大きく異なるため、上記のソースはそのまま使用できないと思います）

# 使い方

厳密さを求めない場合は、下記のようにfirestoreTimestampLooseSchemaのみを使用すればOKです。
firestoreTimestampLooseSchemaは、Firestoreへデータを格納する時に設定する値（[serverTimestamp() で生成した値](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja#server_timestamp)）と、Firestoreからデータを取得したときに入っている値（Timestamp）の両方を受け付けてくれます。

```typescript
import { z } from "zod";
import { firestoreTimestampLooseSchema } from "firebaseSchema";

/** ユーザー情報スキーマ */
const userSchema = z.object({
  /** ユーザー名 */
  name: z.string().max(100).default(""),

  /** 最終ログイン日時 */
  lastLoginedAt: firestoreTimestampLooseSchema,
});

/** ユーザー情報の型 */
export type User = z.infer<typeof userSchema>;
// => {
//     name: string;
//     lastLoginedAt: FieldValue | Timestamp;
// }
```

ただし上記のような定義だと、Firestoreから取得したデータの場合にも、lastLoginedAtフィールドの型が`FieldValue | Timestamp`となります。（この場合、FieldValueが入っている可能性はないはずです）
より厳密さを求めるなら、下記のように「未保存のデータ」と「保存済のデータ（Firestoreから取得したデータ）」を別々のスキーマとして定義してください。

```typescript
import { z } from "zod";
import { firestoreTimestampLooseSchema, firestoreTimestampSchema } from "firebaseSchema";

/** ユーザー情報スキーマの共通プロパティ */
const baseSchema = z.object({
  /** ユーザー名 */
  name: z.string().max(100).default(""),
});

/** ユーザー情報スキーマ（保存済） Firestoreから取得したデータをパースする場合はこちらを使う */
export const storedUserSchema = baseSchema.extend({
  /** 最終ログイン日時 */
  lastLoginedAt: firestoreTimestampSchema,
});

/** ユーザー情報スキーマ（未保存） Firestoreへ格納するためのオブジェクトを生成する場合はこちらを使う */
export const unstoredUserSchema = baseSchema.extend({
  /** 最終ログイン日時 */
  lastLoginedAt: firestoreTimestampLooseSchema,
});

/** ユーザー情報の型 */
export type StoredUser = z.infer<typeof storedUserSchema>;
// => {
//     name: string;
//     lastLoginedAt: Timestamp;
// }

export type UnstoredUser = z.infer<typeof unstoredUserSchema>;
// => {
//     name: string;
//     lastLoginedAt: FieldValue | Timestamp;
// }
```

# おまけ：Firebase v8以前向けのスキーマ定義（未検証）

注：ビルドエラーが出ないことは確認しましたが、動作は検証していないため、動くかどうかは分かりません。


```typescript
import firebase from "firebase";
import { z } from "zod";

/** firestoreのFieldValueオブジェクトを受け付けるスキーマ */
export const firestoreFieldValueSchema =
  z.custom<firebase.firestore.FieldValue>(
    (data) => data instanceof firebase.firestore.FieldValue
  );

/** firestoreのTimestampオブジェクトを受け付けるスキーマ（FieldValueは受け付けない） */
export const firestoreTimestampSchema = z.instanceof(
  firebase.firestore.Timestamp
);

/** firestoreのTimestampオブジェクトまたはFieldValueオブジェクトを受け付けるスキーマ */
export const firestoreTimestampLooseSchema = z.union([
  firestoreFieldValueSchema,
  firestoreTimestampSchema,
]);
```

# 参考資料

FieldValueを受け付けるスキーマの定義については、GitHub内の下記Issueを参考にしています。

* [zod - How to use instanceof to class with private constructor #384](https://github.com/colinhacks/zod/issues/384)


[zod]: https://github.com/colinhacks/zod