-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
z.record
with a key of a union or enum schema results in a partial record
#2623
Comments
As the high level implementation idea, I have the following in mind.
|
Just encountered the same issue. |
I'm using this workaround in the meantime: export function isPlainObject(value: unknown): value is Record<string | number | symbol, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date);
}
export function zodStrictRecord<K extends z.ZodType<string | number | symbol>, V extends z.ZodTypeAny>(
zKey: K,
zValue: V,
) {
return z.custom<Record<z.infer<K>, z.infer<V>>>((input: unknown) => {
return (
isPlainObject(input) &&
Object.entries(input).every(
([key, value]) => zKey.safeParse(key).success && zValue.safeParse(value).success,
)
);
}, 'zodStrictRecord: error');
} |
Facing the same issue here. |
@lunelson your workaround won't catch the missing keys:
I suggest using superRefine to check that all keys are present:
|
@ykolomiets nice catch :). In that case, to be able to handle different types of keys, you'd have to branch on whether the key is an enum, and whether any of the elements in the enum is another schema, or something literal 👍🏼, and this could in theory be nested even further ... probably need a recursive helper for this 🤔 |
Nope, this doesn't work either although it's a cleaner way of implementing my first solution 🤦🏼 export function zRecord<K extends z.ZodType<string | number | symbol>, V extends z.ZodTypeAny>(
zKey: K,
zValue: V,
) {
return z.custom<Record<z.infer<K>, z.infer<V>>>((input: unknown) => z.record(zKey, zValue).safeParse(input).success, 'zodStrictRecord: error');
} |
Just encountered a reverse issue of this: When using EDIT 2024-01-25: Seen this solution somewhere should it help anyone |
+1 |
1 similar comment
+1 |
+1 to this issue. Here's how I achieved this with my own workaround. It both gives an accurate/helpful strongly-typed TypeScript type (via /**
* Zod's `record` when used with an `enum` key type unfortunately makes every key & value optional,
* with no ability to override that or e.g. set `default` values:
* https://github.com/colinhacks/zod/issues/2623
*
* So this helper generates an `object` schema instead, with every key required by default and
* mapped to the given value schema. You can then call `partial()` to behave like Zod's `record`,
* but you can also set `default()` on the value schema to have a default value per omitted key.
* This also achieves an exhaustive key check similar to TypeScript's `Record` type.
*/
export function zodRecordWithEnum<
EnumSchema extends ZodEnum<any>,
EnumType extends z.infer<EnumSchema>,
ValueSchema extends ZodTypeAny,
>(enumSchema: EnumSchema, valueSchema: ValueSchema) {
return z.object(
// TODO: Why is this explicit generic parameter needed / `enumSchema.options` typed as `any`?
_zodShapeWithKeysAndValue<EnumType, ValueSchema>(
enumSchema.options,
valueSchema,
),
)
}
function _zodShapeWithKeysAndValue<
KeyType extends string | number | symbol,
ValueSchema extends ZodTypeAny,
>(keys: KeyType[], valueSchema: ValueSchema) {
return Object.fromEntries(
keys.map(key => [key, valueSchema]),
// HACK: This explicit cast is needed bc `Object.fromEntries()` loses precise typing of keys
// (even with `as [keyof PropsType, ValueType][]` on the `Object.keys(...).map(...)` above).
// Wish Zod had a helper for mapped types similar to TypeScript.
) as {
[Key in KeyType]: ValueSchema
}
} Example usage: const groupEnum = z.enum(['FOO', 'BAR'])
export type Group = z.infer<typeof groupEnum> // "FOO" | "BAR"
const membersSchema = z.array(z.string()).optional().default([])
export type Members = z.infer<typeof membersSchema> // string[]
export const groupMembersSchema = zodRecordWithEnum(
groupEnum,
membersSchema,
)
export type GroupMembers = z.infer<typeof groupMembersSchema>
// ^-- { FOO: string[], BAR: string[] } Both FYI if helpful to anyone else, and feedback welcome if I'm missing anything! Thanks, and thanks @hayata-suenaga for filing this issue. =) (P. S. I'm really loving Zod despite issues like this! 🙂 Thanks very much @colinhacks for such a great library. ❤️) |
+1 export const PosTypeEnumSchema = z.enum(['external', 'online', 'standard', 'telephone']); That way I don't need to define all of my enum schemas manually. Now I need a record with these enums as a key type and I don't want to re-define these keys by hand but instead use the generated |
Wouldn't the correct behaviour here be splitting out the record and the const R = z.record(z.brand("foo"), z.string());
type IR = z.infer<typeof P>;
// IR = Record<Branded, string>
const P = z.keyedObject(z.brand("foo"), z.string());
type IP = z.infer<typeof P>;
// IP = { [key: Branded]: string } Considering that Typescript's |
I solved it by just doing a reduce. Not sure if it's very elegant, but it does the trick. export const l10nTextSchema = z.object(
languageSchema.options.reduce((acc, lang) => {
acc[lang] = z.string()
return acc
}, {} as Record<typeof languageSchema._type, ReturnType<typeof z.string>>)
) |
I too just encountered this I assumed I was doing something wrong (until I found this thread here, hi 👋) The docs imply this should generate It seems worth at least adding a note to the docs about this case, similar to |
Same issue using a branded string as a record key |
Simple workaround for enums: const zodRecord = <
Enum extends z.EnumLike,
ValueParser extends z.ZodTypeAny,
>(
_enum: Enum,
valueParser: ValueParser,
) => {
return z
.object(
Object.fromEntries(Object.values(_enum).map((key) => [key, valueParser])),
)
.transform(
(value) => value as Record<keyof Enum, z.infer<typeof valueParser>>,
);
}; |
If a union or enum schema is passed to
z.record
as a key type, the resulting schema has all properties as optional for both the parsing logic and the inferred TypeScript type.I propose that we make the behavior of
z.record
similar to that of TypeScript's. If you pass an union or enum type to Record in TypeScript, the resulting type has all properties required.I understand changing the existing behavior of
z.schema
would be a breaking change. For now, how about introducing a new zod typez.strictRecord
where all properties are required?I apologize if this has been considered before. Please let me know if there are specific reasons the behavior of
z.schema
differs from TypeScript's 🙇I also found related issues and listed them below for reference.
z.record
makes all his attributes optional #2320The following example code illustrates the current behavior of
z.record
.The following example code illustrates the behavior of TypeScript Record.
If the schema created by
z.record()
is used for a property on another object schema, the property's type is inferred as aPartial
type.The text was updated successfully, but these errors were encountered: