🦭

zodでネストした一部が失敗しても許可する型を作りたい場合

2023/02/02に公開

例えば下記のようなスキーマを考える

const AuthorSchema = z.object({
  name: z.string()
})
const BookSchema = z.object({
  name: z.string(),
  author: AuthorSchema
})

この型において、authorの型が失敗していたら無いものとして無視して扱いたいケースがある場合について、少し工夫が必要だったのでまとめる。

optionalをつける

まず初手として単純にオプションにしたければoptional()をつけることになる

const AuthorSchema = z.object({
    name: z.string()
  })
const BookSchema = z.object({
  name: z.string(),
  author: AuthorSchema.optional()
})

が、例えばauthorが間違っている場合失敗扱いになってしまう

BookSchema.parse({
  name: "book",
  author: {
    authorName: "bob"
  }
})
// => ZodError

catchをつける

失敗時のために.catchを組み合わせる

const AuthorSchema = z.object({
  name: z.string()
})
const BookSchema = z.object({
  name: z.string(),
  author: AuthorSchema.optional().catch(undefined)
})

BookSchema.parse({
  name: "book",
  author: {
    authorName: "bob"
  }
})
// => {name: "book", author: undefined }

これでパース自体はうまくいくようになる。

が、これをz.inferして型を取り出した場合、authorが不足となってしまい、少し都合が悪い

type Book = z.infer<typeof BookSchema>
const book: Book = { 
  name: "foo",
} // => 型エラー

型のために更にoptionalを追加(結論)

型の問題に対処するために、更にoptionalをつける

const AuthorSchema = z.object({
  name: z.string()
})
const BookSchema = z.object({
  name: z.string(),
  author: AuthorSchema.optional().catch(undefined).optional()
})
type Book = z.infer<typeof BookSchema>
const book: Book = {
  name: "foo",
} // 型OK

これでまず目的は解決できた

もう一歩綺麗にする

とはいえ流石に記述が気持ち悪いので、ちょっと微調整する

const AuthorSchema = z.object({
  name: z.string()
})
const OptionalAuthorSchema = AuthorSchema.optional()
const BookSchema = z.object({
  name: z.string(),
  author: OptionalAuthorSchema.catch(undefined).optional()
})

エラーの場合にはnullにしたければ下記のようにしても良い

const OptionalAuthorSchema = AuthorSchema.nullish()
const BookSchema = z.object({
  name: z.string(),
  author: OptionalAuthorSchema.catch(null).optional()
})

エラーの場合はちゃんと見分けれる形にしたいのであれば下記のようなことも可能

const AuthorSchema = z.object({
  name: z.string()
}).or(z.object({
  error: z.literal(true)
}))
const OptionalAuthorSchema = AuthorSchema.catch({ error: true })
const BookSchema = z.object({
  name: z.string(),
  author: OptionalAuthorSchema.optional()
})

Result型っぽくすることも可能だがここまでやるなら他のアプローチを考えたほうが良いかもしれない

const AuthorSchema = z.object({
  name: z.string()
})
type Author = z.infer<typeof AuthorSchema>
const AuthorResultSchema = AuthorSchema
  .transform<{ value: Author, success: true }>(param => ({
    value: param,
    success: true
  })).or(z.object({
    success: z.literal(false)
  }))
  .catch({ success: false })
type AuthorResult = z.infer<typeof AuthorResultSchema>

GitHubで編集を提案

Discussion