Zod Discriminated Union Form Router

Model variant form payloads with Zod discriminated unions, routing each discriminant to its own schema, fields, and submit handler.

TypescriptForm Validation

Code

snippet
import { z } from 'zod';

const emailLogin = z.object({
  method: z.literal('email'),
  email: z.string().email(),
  password: z.string().min(8),
  remember: z.boolean().default(false),
});

const smsLogin = z.object({
  method: z.literal('sms'),
  phone: z.string().regex(/^\+\d{7,15}$/),
  code: z.string().regex(/^\d{6}$/),
});

const oauthLogin = z.object({
  method: z.literal('oauth'),
  provider: z.enum(['github', 'google']),
  idToken: z.string().min(20),
});

const LoginPayload = z.discriminatedUnion('method', [
  emailLogin, smsLogin, oauthLogin,
]);

type LoginPayload = z.infer<typeof LoginPayload>;

const handlers: Record<LoginPayload['method'], (p: never) => string> = {
  email: (p) => 'signing in with ' + (p as z.infer<typeof emailLogin>).email,
  sms: (p) => 'verifying sms for ' + (p as z.infer<typeof smsLogin>).phone,
  oauth: (p) => 'oauth via ' + (p as z.infer<typeof oauthLogin>).provider,
};

function submit(raw: unknown): string {
  const parsed = LoginPayload.parse(raw);
  return handlers[parsed.method](parsed as never);
}

console.log(submit({ method: 'email', email: '[email protected]', password: 'hunter2xx' }));
console.log(submit({ method: 'sms', phone: '+8613800138000', code: '123456' }));