Zod 4 regression: parsing with schema of indexed access type
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
Did you find a solution? @andersk ???
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 @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.
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
Here's another example that seems like the same issue:
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>;