Skip to content

fix(drizzle-zod): give createSchemaFactory's TCoerce generic a default, fixing refine callback any (#5968)#6031

Open
dumbCodesOnly wants to merge 1 commit into
drizzle-team:mainfrom
dumbCodesOnly:fix/5968-schema-factory-tcoerce-default
Open

fix(drizzle-zod): give createSchemaFactory's TCoerce generic a default, fixing refine callback any (#5968)#6031
dumbCodesOnly wants to merge 1 commit into
drizzle-team:mainfrom
dumbCodesOnly:fix/5968-schema-factory-tcoerce-default

Conversation

@dumbCodesOnly

Copy link
Copy Markdown

Fixes #5968.

Root cause

createInsertSchema / createSelectSchema / createUpdateSchema at the top level are hardcoded consts typed <undefined> for their TCoerce parameter. But createSchemaFactory<TCoerce extends CoerceOptions>(options?) is generic with no default.

When the factory is called without a coerce option — e.g.

const { createInsertSchema } = createSchemaFactory({ zodInstance: z });

TypeScript has no call-site candidate to infer TCoerce from (only zodInstance is passed), so it falls back to the full constraint union (Partial<Record<'bigint'|'boolean'|'date'|'number'|'string', true>> | true | undefined) instead of undefined.

That wider TCoerce flows into BuildRefineGetZodTypeCanCoerce, whose conditional types distribute over the union, and the refine callback's schema parameter collapses to any (TS7006 under noImplicitAny/strict):

export const projectInsertSchema = createInsertSchema(project, {
  title: (schema) => schema.min(1).max(200),
  //      ^ implicitly any
});

Correction to the original report: this isn't specific to extended Zod instances (e.g. @hono/zod-openapi's z). It reproduces with any createSchemaFactory call that omits coerce, including plain zod/v4. The extended-instance case in the original issue just happened to be the one that surfaced it — I verified factory + plain zod hits the identical TS7006 on the real published package.

Fix

Give TCoerce a default of undefined, matching the implicit default the hardcoded top-level consts already have:

export function createSchemaFactory<
	TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined =
		undefined,
>(options?: CreateSchemaFactoryOptions<TCoerce>) {

Verification

Tested against the real built package (patched .d.ts/.d.cts via patch-package since I verified the type-level cause there first) with real tsc --strict --noImplicitAny:

  • Before: createSchemaFactory({ zodInstance: z }) — any zod instance, factory omitting coerce — refine callback schema is any.
  • After: refine callback schema is a properly typed ZodType in all cases: extended instance, plain zod, and also with an explicit coerce option (e.g. coerce: { number: true } still correctly infers a coerced-number schema — the default doesn't regress explicit coerce usage).

Full repro + CI verification (4 cases — original repro, plain-zod-factory control, no-factory control, explicit-coerce control — all clean, workflow green):

Happy to add a type-test case under drizzle-zod/tests if maintainers want one in-repo rather than just linked externally — let me know the preferred location/pattern and I'll add it.

…fine callback to type as `any`

Fixes drizzle-team#5968.

createInsertSchema/createSelectSchema/createUpdateSchema at the top level
are hardcoded consts typed <undefined> for their TCoerce parameter. But
createSchemaFactory<TCoerce extends CoerceOptions>(options?) is generic
with no default. When the factory is called without a `coerce` option
(e.g. `createSchemaFactory({ zodInstance: z })`), TypeScript has no call-site
candidate to infer TCoerce from, and falls back to the full constraint
union (Partial<Record<'bigint'|'boolean'|'date'|'number'|'string', true>>
| true | undefined) instead of `undefined`. That wider TCoerce flows into
BuildRefine -> GetZodType -> CanCoerce, whose conditional types distribute
over the union, and the refine callback's `schema` parameter collapses to
`any` (TS7006 under noImplicitAny/strict).

This reproduces with ANY createSchemaFactory call that omits `coerce`,
regardless of which zod instance is passed (plain zod/v4 or an extended
instance like @hono/zod-openapi's z) -- it isn't specific to extended
instances as originally reported; the reporter's extended-instance case
just happened to be the one that surfaced it.

Fix: give TCoerce a default of `undefined`, matching the implicit default
the hardcoded top-level consts already have.

Verified against the built package (patched .d.ts/.d.cts via
patch-package) with real tsc:
- Before: `createSchemaFactory({ zodInstance: z })` (any zod instance)
  -> refine callback schema is `any`, TS7006 under --strict --noImplicitAny.
- After: refine callback schema is a properly typed ZodType in all cases,
  including with an extended instance, with plain zod, and with an
  explicit `coerce` option (e.g. `coerce: { number: true }` still infers
  a coerced-number schema correctly, not `any` -- the default doesn't
  regress explicit coerce usage).

Full repro + CI verification (all 4 cases, all clean, workflow green):
https://github.com/dumbCodesOnly/drizzle-issue-5968-repro
Run: https://github.com/dumbCodesOnly/drizzle-issue-5968-repro/actions/runs/29641936968
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: drizzle-orm/zod createSchemaFactory breaks refine callback type inference (implicit 'any')

1 participant