🐶

ZodIntersectionのプロパティの和型のうち、いらない型を`.and()`を使って除外する

2024/10/31に公開

ZodIntersectionのプロパティの和型のうち、いらない型を.and()を使って除外する

// やんごとなき理由でZodIntersectionになっている(.omit()や.extend()が使えない)zodスキーマ
const xSchema = z
  .object({
    a: z.number().nullable(), // この`number | null`を`number`に制限して、新しいスキーマを作りたい
  })
  .and(
    z.object({
      b: z.string(),
    })
  );

// nullableを消すだけなら、実は.and()で足りる
const ySchema = xSchema.and(
  z.object({
    a: z.number(),
  })
);

type Y = z.infer<typeof ySchema>;
type A = Y['a']; // number

ZodIntersectionは.omit().extend()ができない

ZodObjectなどのzodの主要な型は.omit().extend()ができるので、スキーマのプロパティの型を上書きできます。
例えば以下のような用法です。

const fooSchema = z.object({
  baz: z.number(),
})
const barSchema = fooSchema.extend({
  baz: z.string(),
})

// あるいは
const barSchema = fooSchema
  .omit({ baz: true })
  .and(z.object({
    baz: z.string(),
  }))

しかしやんごとなき理由(例えばライブラリ側でzodスキーマが提供されているなどの理由)でZodIntersectionのみが見えている場合、ZodIntersectionには.omit().extend()がはえていないので、上述のような上書きはできません。

ただ今回「和型の部分的除外ならできる」という気づきがあったので、共有します。

ZodIntersectionの和型プロパティを部分的に除外する

まず以下のようなZodIntersectionがあるとします。

// やんごとなき理由でZodIntersectionになっている(.omit()や.extend()が使えない)zodスキーマ
const xSchema = z
  .object({
    a: z.number().nullable(), // この`number | null`を`number`に制限して、新しいスキーマを作りたい
  })
  .and(
    z.object({
      b: z.string(),
    })
  );

.omit().extend()はできません。

// ZodIntersectionには.omit()がないのでできない
// const ySchema = xSchema.omit({ a: true }).and(z.object({
//   a: z.number()
// }))

// ZodIntersectionには.extend()がないのでできない
// const ySchema = xSchema.extend({
//   a: z.number()
// })

このとき、以下のようにすることで、.anumber | nullからnumberにすることができます。

const ySchema = xSchema.and(
  z.object({
    a: z.number(),
  })
);

type Y = z.infer<typeof ySchema>;
type A = Y['a']; // number

実はそれはそうで、これはzodスキーマに限った話ではなく、TypeScriptの素の型でそうなっています。

type _X = {
  a: number | null;
};
type _Y = _X & {
  a: number;
};
type _A = _Y['a']; // number

以下はその計算式です。

{ a: number | null } & { a: number }
= { a: (number | null) & number }
= { a: (number & number) | (null & number) }
= { a: number | never }
= { a: number }

この除外(和型の一部を取り出す方法)は、もちろん除外対象がnullでなくとも使えます。

const wSchema = z
  .object({
    a: z.union([z.number(), z.literal('p'), z.boolean(), z.literal('q')]),
  })
  .and(
    z.object({
      b: z.string(),
    })
  );
const vSchema = wSchema.and(
  z.object({
    a: z.union([z.number(), z.boolean()]), // 'p'と'q'を除外する
  })
);
type V = z.infer<typeof vSchema>;
type VA = V['a']; // number | boolean

要は上述の

const wSchema = z
  .object({
    a: /* ここ(I) */,
  })
  .and(/* 割愛 */);

const vSchema = wSchema.and(
  z.object({
    a: /* ここ(J) */,
  })
);

このJがIの親型になっていればよいです。

上書きはできない

ただしもちろん、上書きはできないです。

const zSchema = xSchema.and(
  z.object({
    a: z.string(),
  })
);
type ZA = z.infer<typeof zSchema>['a']; // never

この場合はa: neverになります。

Discussion