zod
zod copied to clipboard
When z.infer a Record with Template Literal Type as a key, it becomes a Partial Record.
This issue is very similar to #2069 but instead of Branded key, the same issue is with Template Literal Type key.
When z.infer a Record with Template Literal Type as a key, it becomes a Partial Record.
With defining a Record type normally:
import { z } from "zod"
const zodKey = z.custom<`${number}`>()
type ZodKey = z.infer<typeof zodKey>
type ZodRecord = Record<ZodKey, number>
// type ZodRecord = {
// [x: `${number}`]: number;
// }
With defining a Record type with z.infer
:
import { z } from "zod"
const zodKey = z.custom<`${number}`>()
const zodRecord = z.record(zodKey, z.number())
type ZodRecord = z.infer<typeof zodRecord>
// type ZodRecord = {
// [x: `${number}`]: number | undefined;
// }
It has undefined
.
The extra Partial<...> is more explicit in this case:
import { z } from "zod"
const zodKey = z.custom<`${number}`>()
const zodRecord = z.record(zodKey, z.object({ a: z.number(), b: z.number() }))
const zodWrapper = z.object({ zodRecord })
type ZodWrapper = z.infer<typeof zodWrapper>
// type ZodWrapper = {
// zodRecord: Partial<Record<`${number}`, {
// a: number;
// b: number;
// }>>;
// }
I have the same issue. It doesn't work in the same way as plain TS types
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Still an issue.
Recently ran into this issue as well. Was quite befuddling to uncover haha!
I've offered a workaround using z.custom
here: https://github.com/colinhacks/zod/issues/2623#issuecomment-1849464377
My workaround:
z.record(branded, other).transform((x) => x as typeof x extends Partial<infer T> ? T : never);
This issue actually also exists for union types.
z.record(z.literal("a").or(z.literal("b")), z.number());
resolves to
{
a?: number | undefined;
b?: number | undefined;
}
Rather than the expected
Record<"a"|"b", number>
// or
{
a: number;
b: number;
}
The workaround suggested by @rijenkii works in this case as well. Building further on his suggestion, there's actually a utility type for this, introduced in typescript 2.8. An improved workaround would look like this:
z.record(branded, other).transform((x) => x as Required<typeof x>);
The same is achievable through refine:
z.record(branded, other).refine((arg): arg is Required<typeof arg> => true)
// or
z.record(branded, other).refine(<T>(arg: T): arg is Required<T> => true)
@rijenkii 's solution was the only one to work for me...
Here's a reduced example of my code:
export const SCHEMA = {
response: z.object({
edges: z
.record(
z.custom<Uppercase<string>>((val) => typeof val === 'string' && val === val.toUpperCase()),
z.object({}),
)
.transform((x) => x as typeof x extends Partial<infer T> ? T : never),
}),
};
export type SchemaResponse = z.infer<(typeof SCHEMA)['response']>;
And the correctly inferred type:
type SchemaResponse = {
edges: Record<Uppercase<string>, {}>;
}
Without the .transform
line the type is:
type SchemaResponse = {
edges: Partial<Record<Uppercase<string>, {}>>;
}
And with the refine
approach or using Required
in transform
the type ends up as:
type SchemaResponse = {
edges: Required<Partial<Record<Uppercase<string>, {}>>>;
}