TypeScript 6 + Zod v4: ReturnType<typeof Schema.parse> Collapses to unknown

Published: 2026-06-26

Dependabot opened four upgrade PRs at once. I merged them one by one: a NestJS patch, then the dev-tooling group (TypeScript 5.9.3 → 6.0.3, ESLint 9 → 10), then the production-deps group (Zod 3.25.76 → 4.4.3).

Each PR looked fine in isolation. No CI failures. No obvious API changes in the CHANGELOGs that affected my code.

Then I ran pnpm build.

Seven TypeScript errors. Most were straightforward — new strictness rules, a renamed property. One wasn’t.


The Symptom

The error message was:

Type 'unknown' is not assignable to type 'string'.

Coming from this line:

async createAlert(dto: ReturnType<typeof CreateAlertSchema.parse>) {
  // ... dto.userId was typed as unknown, not string
}

Nothing had changed in CreateAlertSchema. Nothing had changed in the function signature. The only visible fact was that something that used to be string was now unknown.

This is a secondary symptom. The compiler is reporting a downstream consequence, not the root cause.


Tracing It Back

CreateAlertSchema is a Zod schema defined in a shared package:

// packages/schemas/src/alert.schema.ts
import { z } from 'zod';

export const CreateAlertSchema = z.object({
  userId: z.string(),
  symbol: z.string(),
  threshold: z.number()
});

export type CreateAlertDto = z.infer<typeof CreateAlertSchema>;

The ReturnType<typeof CreateAlertSchema.parse> pattern extracts the schema’s output type directly from the parse method — a common shortcut to avoid a separate type export.

It worked in Zod v3 with TypeScript 5. After merging both upgrade PRs, it returned unknown.

To understand why, I had to look at what each upgrade changed independently.


What Zod v4 Changed

In Zod v3, parse() was typed roughly as:

parse(data: unknown): Output;

In Zod v4, it changed to1:

parse(data: unknown): core.output<this>;

The key is what core.output<T> actually expands to2:

type output<T> = T extends { _zod: { output: any } } ? T['_zod']['output'] : unknown;

If T resolves to a concrete schema with a known _zod.output, you get the schema’s output type. If it can’t be resolved — the else-branch fires, and the else-branch is unknown.

By itself, the change is fine. z.infer<typeof CreateAlertSchema> still works — it calls output<T> with the concrete schema type, which satisfies the condition. The issue is specifically with ReturnType<>.


What TypeScript 6.0 Changed

TypeScript 6.0 made strict: true the default3, which activates strictPropertyInitialization, noImplicitAny, and related flags. Several other compile errors in this same upgrade — uninitialized class properties in NestJS services — came from there.

For the ReturnType<typeof Schema.parse> collapse specifically, no documented TypeScript 6.0 change covers this behavior. The mechanism is entirely in Zod v4’s type definition described above.


Why ReturnType<> Fails Here

ReturnType<typeof Schema.parse> asks TypeScript to resolve the return type of parse without calling it. The return type is core.output<this> — a conditional type that depends on the concrete schema’s internal structure. In certain project configurations (monorepo setups, compiled .d.ts across package boundaries, specific tsconfig combinations), the condition can’t be satisfied and falls through to the else-branch: unknown.

In a simple standalone project the issue may not reproduce. In the NestJS monorepo where this occurred, it did — consistently, after merging both upgrade PRs.

The bug surfaced here because both upgrades landed simultaneously (TypeScript and Zod in separate Dependabot groups, merged back to back). CI passes because type inference failure isn’t a compile error on its own until it causes a downstream mismatch.

The best signal is when pnpm build introduces failures on code you didn’t touch.


The Fix

Stop relying on ReturnType<typeof Schema.parse> to carry the type. Use the exported type directly:

// Before — collapses to unknown with TS6 + Zod v4
async createAlert(dto: ReturnType<typeof CreateAlertSchema.parse>) { ... }

// After — explicit, stable
async createAlert(dto: CreateAlertDto) { ... }

If you weren’t exporting a named type alongside the schema, add one:

export const CreateAlertSchema = z.object({ ... });
export type CreateAlertDto = z.infer<typeof CreateAlertSchema>;

z.infer<> operates at the type level via conditional types, not through method signature extraction. It’s unaffected by the ReturnType<> + this-polymorphic combination.


Where Else This Pattern Hides

If you’re upgrading both TypeScript 6.0 and Zod v4, grep for any use of ReturnType<typeof alongside Zod schemas:

grep -r "ReturnType<typeof" src/ --include="*.ts"

Common patterns to look for:

// All of these collapse to unknown with Zod v4

type Parsed = ReturnType<typeof MySchema.parse>;
let dto: ReturnType<typeof RequestSchema.parse>;
function handle(body: ReturnType<typeof BodySchema.parse>) { ... }

The fix is the same in each case: replace with z.infer<typeof MySchema> or a named type exported from the schema file.

ReturnType<typeof Schema.safeParse> is also affected — it would resolve to ZodSafeParseResult<unknown> instead of the concrete form. Avoid it for the same reason.


Summary

Zod v3Zod v4
parse() return typeOutputcore.output<this>
ReturnType<typeof Schema.parse>Correct typeCollapses to unknown

If you’re upgrading via Dependabot and TypeScript and Zod land in separate groups, you won’t see this coming from the CHANGELOGs alone. Build after each merge — type-level regressions are invisible to tests.


Further Reading

Footnotes

  1. parse() method signature in Zod v4 — packages/zod/src/v4/classic/schemas.ts#L116.

  2. output<T> conditional type definition in Zod v4 — packages/zod/src/v4/core/core.ts#L118.

  3. TypeScript 6.0 release notes — Simple Default Changes. The strict: true default is documented here.