You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Zod默认将所有Schema字段设为可选?如何在Express+TypeScript+Mongoose项目中解决其导致的类型冲突

Fixing Zod Type Conflict in Express + TypeScript + Mongoose API

Hey there! Let's clear up this confusion and fix your type issue step by step. First off: Zod does NOT default to making schema fields optional—your createUserSchema already defines all fields as required (since you're using string() without the .optional() modifier). The root of your type conflict is actually a mistake in how you're defining the CreateUserInput type.

Why You're Seeing the Optional Type Error

Your current CreateUserInput definition uses:

export type CreateUserInput = Omit<TypeOf<typeof createUserSchema>, 'body.passwordConfirmation'>;

But here's the problem:

  • TypeOf<typeof createUserSchema> resolves to an object with a single top-level body property (since your schema wraps all fields inside body: object(...)).
  • The Omit utility only accepts top-level property names—you can't use nested paths like 'body.passwordConfirmation'. This means your Omit does nothing, and CreateUserInput is still the full schema type ({ body: { ...all fields... } }).

When you try to assign req.body (which Express infers as having optional properties by default) to your UserInput interface (which requires all fields), TypeScript throws the conflict error because it sees the unvalidated request body's properties as optional.

How to Fix It

1. Correctly Define CreateUserInput

You need to first extract the type of the body object from your schema, then omit the passwordConfirmation field:

import { TypeOf, object, string } from 'zod';

export const createUserSchema = object({
  body: object({
    firstName: string({ required_error: 'First name is required' }),
    lastName: string({ required_error: 'Last name is required' }).nonempty(),
    password: string({ required_error: 'Password is required' })
     .nonempty()
     .min(6, 'Password too short - should be 6 chars minimum'),
    passwordConfirmation: string({ required_error: 'Confirm password is required' }),
    email: string({ required_error: 'Email is required' })
     .email('Not a valid email')
     .nonempty(),
  }).refine((data) => data.password === data.passwordConfirmation, {
    message: 'Passwords do not match',
    path: ['passwordConfirmation'],
  }),
});

// Correct type extraction
export type CreateUserInput = Omit<TypeOf<typeof createUserSchema>['body'], 'passwordConfirmation'>;

This CreateUserInput will now exactly match your UserInput interface:

{
  firstName: string;
  lastName: string;
  password: string;
  email: string;
}

All fields are required, no optional flags in sight.

2. Validate Request Body with Zod to Get Proper Types

In your Express route handler, use Zod's parse or safeParse to validate the request body—this ensures TypeScript infers the correct required types:

Using parse (throws error on invalid input):

import { Request, Response } from 'express';

export const createUser = (req: Request, res: Response) => {
  try {
    // Validate the entire request (including body)
    const validatedRequest = createUserSchema.parse(req);
    
    // Extract the user input fields (type-safe!)
    const userInput: CreateUserInput = {
      firstName: validatedRequest.body.firstName,
      lastName: validatedRequest.body.lastName,
      password: validatedRequest.body.password,
      email: validatedRequest.body.email,
    };

    // Pass userInput to Mongoose for user creation
    // User.create(userInput)...

    res.status(201).json({ message: 'User created' });
  } catch (error) {
    res.status(400).json({ errors: error.issues });
  }
};

Using safeParse (returns error object instead of throwing):

export const createUser = (req: Request, res: Response) => {
  const validationResult = createUserSchema.safeParse(req);
  
  if (!validationResult.success) {
    return res.status(400).json({ errors: validationResult.error.issues });
  }

  const userInput: CreateUserInput = {
    firstName: validationResult.data.body.firstName,
    lastName: validationResult.data.body.lastName,
    password: validationResult.data.body.password,
    email: validationResult.data.body.email,
  };

  // Proceed with user creation...
  res.status(201).json({ message: 'User created' });
};

Bonus: Eliminate Redundant UserInput Interface

Since CreateUserInput now perfectly represents the required user input fields, you can delete the separate UserInput interface entirely—no need to maintain duplicate type definitions!

Key Takeaways

  • Zod fields are required by default; you have to explicitly add .optional() to make a field optional.
  • Your type conflict came from incorrect nested type extraction, not Zod's default behavior.
  • Always validate request data with Zod in your route handlers to get type-safe data that matches your schema.

内容的提问来源于stack exchange,提问作者Ali H. Kudeir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 12:59:09