zod icon indicating copy to clipboard operation
zod copied to clipboard

Zod 4 regression: parsing with schema of indexed access type

Open andersk opened this issue 4 months ago • 4 comments

I’m using indexed access types to select between several Zod schemas of different output types depending on a key. Using TypeScript 5.9.2, this type checks successfully with Zod 3.25.76 but not with Zod 4.0.0 and later:

import * as z from "zod";

const schemas = { a: z.string(), b: z.number(), c: z.boolean() };
type Schemas = typeof schemas;
function p<K extends keyof Schemas>(k: K, v: unknown): z.output<Schemas[K]> {
  return schemas[k].parse(v);
}

With Zod 3, since schemas[k] has type Schemas[K], TypeScript correctly accepts schemas[k].parse(v) at type z.output<Schemas[K]>. But with Zod 4, TypeScript weakens its type to string | number | boolean, where the correlation with K has been lost, leading to this error:

test.ts:6:3 - error TS2322: Type 'string | number | boolean' is not assignable to type 'output<{ a: ZodString; b: ZodNumber; c: ZodBoolean; }[K]>'.
  Type 'string' is not assignable to type 'output<{ a: ZodString; b: ZodNumber; c: ZodBoolean; }[K]>'.

6   return schemas[k].parse(v);
    ~~~~~~


Found 1 error in test.ts:6

andersk avatar Aug 26 '25 00:08 andersk

Did you find a solution? @andersk ???

JoshBashed avatar Aug 28 '25 04:08 JoshBashed

Unclear. TypeScript accepts this apparent workaround:

import * as z from "zod";

const schemas = { a: z.string(), b: z.number(), c: z.boolean() };
type Schemas = typeof schemas;
function p<K extends keyof Schemas>(k: K, v: unknown): z.output<Schemas[K]> {
  const typed_schemas: {
    [K in keyof Schemas]: z.ZodType<z.output<Schemas[K]>, z.input<Schemas[K]>>;
  } = schemas;
  return typed_schemas[k].parse(v);
}

However, I’m not sure if it’s taking advantage of some bug in TypeScript that will break in the future, because this blatantly unsound mistake is also accepted:

function q<K extends keyof Schemas>(k: K, v: unknown): z.output<Schemas[K]> {
  const wrong_schemas: {
    [K in keyof Schemas]: z.ZodType<unknown, unknown>;
  } = { a: z.null(), b: z.null(), c: z.null() };
  return wrong_schemas[k].parse(v);
}
const a: string = q("a", null);
console.log(a); // null

andersk avatar Aug 28 '25 22:08 andersk

@andersk @JoshuaBrest not sure why this works, but it does.

My guess is z.output does more than simply access the _output property in v4, and that the operation it performs no longer preserves structure in a way the compiler can understand.

Playground

import * as z from "zod"

const schemas = { a: z.string(), b: z.number(), c: z.boolean() }
type Schemas = typeof schemas

function p<K extends keyof Schemas>(k: K, v: unknown): typeof schemas[K]['_output'] {
  return schemas[k].parse(v)
}

declare const unknown: unknown

const ex_01 = p('a', unknown)
//    ^? const ex_01: string

const ex_02 = p('b', unknown)
//    ^? const ex_02: number

const ex_03 = p('c', unknown)
//    ^? const ex_03: boolean

const ex_04 = p('c', null)
//    ^? const ex_04: boolean

ahrjarrett avatar Aug 31 '25 00:08 ahrjarrett

Here's another example that seems like the same issue:

Playground Link

import * as z from "zod"

const a = z.string();
const b = z.number();
type Schemas = typeof a | typeof b;

function parse<S extends Schemas>(schema: S, v: unknown): z.output<S> {
  return schema.parse(v);
}

The TS error is directly on the return statement. TS is saying the return type of schema.parse(v) is string | number but I expect the return type to be z.output<S> because schema is type S.

The error only goes away if I change the return statement to: return schema.parse(v) as z.output<S>;

nwoltman avatar Dec 04 '25 00:12 nwoltman