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 buildintroduces 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 v3 | Zod v4 | |
|---|---|---|
parse() return type | Output | core.output<this> |
ReturnType<typeof Schema.parse> | Correct type | Collapses 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
- Zod v4 migration guide — covers the generic structure changes to
ZodType<Output, Input>and the broader API overhaul in v4. - TypeScript 6.0 release notes — covers strictness defaults,
rootDirenforcement, and deprecations.
Footnotes
-
parse()method signature in Zod v4 —packages/zod/src/v4/classic/schemas.ts#L116. ↩ -
output<T>conditional type definition in Zod v4 —packages/zod/src/v4/core/core.ts#L118. ↩ -
TypeScript 6.0 release notes — Simple Default Changes. The
strict: truedefault is documented here. ↩