[PR #5962] [CLOSED] fix(core): move email transform to schema level to fix type inference #6349

Closed
opened 2026-03-13 12:55:38 -05:00 by GiteaMirror · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/better-auth/better-auth/pull/5962
Author: @rumeshudash
Created: 11/13/2025
Status: Closed

Base: canaryHead: fix/core-userschema-transform


📝 Commits (2)

  • 15afe87 fix(core): move email transform to schema level to fix type inference
  • f6f4c15 fix(core): linting issue in userSchema.

📊 Changes

1 file changed (+11 additions, -6 deletions)

View changed files

📝 packages/core/src/db/schema/user.ts (+11 -6)

📄 Description

Fix: z.transform generates optional fields when inferring on build TypeScript files

Description

Fixes a TypeScript type inference issue where z.transform() on the user schema causes the email field to become optional in built .d.ts files, even though it's defined as required. This causes type errors in plugins (like the admin plugin) that expect email to be a required field.

Problem

When using z.transform() on a Zod schema, TypeScript can lose type information about required fields during type inference, especially when inferring from compiled .d.ts files. This results in fields that should be required (like email) being incorrectly inferred as optional.

The admin plugin's databaseHooks.user.create.before hook expects email to be required:

async before(user) {
  // user.email should be string, but TypeScript infers it as string | undefined
}

But the inferred type from z.infer<typeof userSchema> makes email optional, causing a type mismatch.

Solution

Moved the email transformation from the field level to the schema level. When using z.transform() on an individual field within z.object().extend(), TypeScript can incorrectly infer fields as optional in built .d.ts files. By moving the transform to the entire schema object, the type inference correctly preserves required fields like email. This approach splits the transform into a separate schema-level operation rather than applying it at the field level.

Changes

  • File: packages/core/src/db/schema/user.ts
  • Change: Moved transform() from the email field to the entire schema
// Before
export const userSchema = coreSchema.extend({
	email: z.string().transform((val) => val.toLowerCase()),
	emailVerified: z.boolean().default(false),
	name: z.string(),
	image: z.string().nullish(),
});

// After
export const userSchema = coreSchema.extend({
	email: z.string(),
	emailVerified: z.boolean().default(false),
	name: z.string(),
	image: z.string().nullish(),
}).transform((data) => ({
	...data,
	email: data.email.toLowerCase()
}));

Testing

  • TypeScript compilation passes without errors
  • Admin plugin type checks correctly with required email field
  • No breaking changes to existing functionality

Closes #5047

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Summary by cubic

Moved email normalization to a schema-level transform so TypeScript keeps email required in built .d.ts inference, while still lowercasing emails at runtime. Fixes optional-email type errors in consumers like the admin plugin.

Written for commit f6f4c15c80. Summary will update automatically on new commits.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/better-auth/better-auth/pull/5962 **Author:** [@rumeshudash](https://github.com/rumeshudash) **Created:** 11/13/2025 **Status:** ❌ Closed **Base:** `canary` ← **Head:** `fix/core-userschema-transform` --- ### 📝 Commits (2) - [`15afe87`](https://github.com/better-auth/better-auth/commit/15afe872b7518c8da717cb4f152f3f1f6611c626) fix(core): move email transform to schema level to fix type inference - [`f6f4c15`](https://github.com/better-auth/better-auth/commit/f6f4c15c806c3b08aa404e375b763831714db6ae) fix(core): linting issue in userSchema. ### 📊 Changes **1 file changed** (+11 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `packages/core/src/db/schema/user.ts` (+11 -6) </details> ### 📄 Description # Fix: z.transform generates optional fields when inferring on build TypeScript files ## Description Fixes a TypeScript type inference issue where `z.transform()` on the user schema causes the `email` field to become optional in built `.d.ts` files, even though it's defined as required. This causes type errors in plugins (like the admin plugin) that expect `email` to be a required field. ## Problem When using `z.transform()` on a Zod schema, TypeScript can lose type information about required fields during type inference, especially when inferring from compiled `.d.ts` files. This results in fields that should be required (like `email`) being incorrectly inferred as optional. The admin plugin's `databaseHooks.user.create.before` hook expects `email` to be required: ```typescript async before(user) { // user.email should be string, but TypeScript infers it as string | undefined } ``` But the inferred type from `z.infer<typeof userSchema>` makes `email` optional, causing a type mismatch. ## Solution Moved the email transformation from the field level to the schema level. When using `z.transform()` on an individual field within `z.object().extend()`, TypeScript can incorrectly infer fields as optional in built `.d.ts` files. By moving the transform to the entire schema object, the type inference correctly preserves required fields like `email`. This approach splits the transform into a separate schema-level operation rather than applying it at the field level. ## Changes - **File**: `packages/core/src/db/schema/user.ts` - **Change**: Moved `transform()` from the email field to the entire schema ```typescript // Before export const userSchema = coreSchema.extend({ email: z.string().transform((val) => val.toLowerCase()), emailVerified: z.boolean().default(false), name: z.string(), image: z.string().nullish(), }); // After export const userSchema = coreSchema.extend({ email: z.string(), emailVerified: z.boolean().default(false), name: z.string(), image: z.string().nullish(), }).transform((data) => ({ ...data, email: data.email.toLowerCase() })); ``` ## Testing - ✅ TypeScript compilation passes without errors - ✅ Admin plugin type checks correctly with required `email` field - ✅ No breaking changes to existing functionality ## Related Issues Closes #5047 ## Type of Change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Moved email normalization to a schema-level transform so TypeScript keeps email required in built .d.ts inference, while still lowercasing emails at runtime. Fixes optional-email type errors in consumers like the admin plugin. <sup>Written for commit f6f4c15c806c3b08aa404e375b763831714db6ae. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. --> --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
GiteaMirror added the pull-request label 2026-03-13 12:55:38 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#6349