-
-
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
Fix branded Record keys in ZodRecord #2287
Conversation
✅ Deploy Preview for guileless-rolypoly-866f8a ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome thank you! This will bring back the TypeScript compile time safety that we're desperately missing 🙂
Many thanks for approving this! Super happy to get this annoyance fixed :) |
Just to clarify for myself, when can I expect this to get merged and published? |
@sangxxh Hey! Could we get this merged soon? |
Unfortunately this is currently intentional. We need the const schema = z.record(z.enum(['a', 'b']), z.number());
type schema = z.infer<typeof schema>;
schema.parse({ a: 5 }); // passes
var x: schema = { a: 5 } // missing key 'b' I agree this isn't ideal, and this will likely change in Zod v4. |
Ahh I see, I guess I never figured out what the original reason for this weird type is. The problem I'm having with it is that it's messing up records that are keyed by a branded string. These cases are not meant to have partial applied to them, right? This line probably was an earlier attempt at fixing this, however it doesn't work for me. Do you have an idea on how to fix this? const schema = z.record(z.string().brand('some-branded-string'), z.number())
type schema = z.infer<typeof schema> // expected Record<string & BRAND<'some-branded-string'>, number>, got Partial<Record<string & BRAND<'some-branded-string'>, number>> |
It is also breaking usage of The following does not work (extracted from zod code, conditionally mapping record type): type Test<K> = [z.BRAND<string | number | symbol>] extends [K] ? true : false
type Key = string & z.BRAND<'Key'>
type R = Test<Key> // = false Here's why: For Hence, the relation must be rotated: type Test<K> = [K] extends [z.BRAND<string | number | symbol>] ? true : false
type Key = string & z.BRAND<'Key'>
type R = Test<Key> // = true Funny thing is, that non-finite keys, for example Which is absurd. Can we at least get the fix through for brand: export declare type RecordType<K extends string | number | symbol, V> = [
string
] extends [K]
? Record<K, V>
: [number] extends [K]
? Record<K, V>
: [symbol] extends [K]
? Record<K, V>
: [K] extends [BRAND<string | number | symbol>]
? Record<K, V>
: Partial<Record<K, V>> |
9cdf371
to
6563e80
Compare
Thanks for your comment @akomm! That makes a lot of sense as to why the earlier merged fix didn't really fix the problem. I've included your suggested solution in a commit on this branch now, to see how it passes the test suite. I'll do testing on my codebase tomorrow (UTC+3) with this idea, and let you all know |
f1c6828
to
cc9297f
Compare
Because of a quirk of the extends syntax of typescript conditional types, the previous version of this check for a branded key didn't work as expected. Thank you @akomm for the explanation of why the extends sides need to be swapped at colinhacks#2287 (comment) Originally I intended to remove the RecordType type entirely, since it would seem initially that it is only there to work around earlier versions of typescript not having the `noUncheckedIndexedAccess` option. However, it is really meant to work around a problem with using ZodEnum as the key, see this comment: colinhacks#2287 (comment)
cc9297f
to
28dc5c6
Compare
I've verified that the change suggested by @akomm does fix the problem we're having in our codebase, so I've amended the branch to fix that one check only, so that this won't affect the case with ZodEnum @colinhacks would this be ok to merge now |
What's the update here, @colinhacks? |
Is there something besides "no time" blocking this? Some doubts or issues? Maybe we can help. |
ping @colinhacks. is there anything I could do to help this get merged? |
@googol do you have a sense of how a |
Unfortunately I have nothing :/ |
Having partial records when the keys are a literal union is by design. It reflects the way zod parses the records : it does not make sure that every value of the union is represented in the record's keys. I have not given many thoughts to this, but maybe using maps instead if records in such use cases is the way to go. |
ZOD does not seem to be very exact about doing what you say, as I've explained in this comment: #2287 (comment) Using an infinite set (=type string) as key results in a non-partial Record. Example from the above comment: TS Playground For obvious reason, ZOD can not test that all infinite possible keys are actually present. Yet the type suggests otherwise. This issue has a long history actually. There were multiple topics regarding the I make it short, after a lot of testing and trying, turns out My argument for this approach is that it would mimic 1:1 TS type safety level to runtime safety level in zod. If you disable afterthought |
There's clearly a lot of confusion about why ZodRecord is the way it is. The comment by @fwoelffel is exactly right:
It's impossible (in the general case) for Zod to take an arbitrary schema and make a list of literals that will pass validation. As such, it isn't possible for Zod to do exhaustiveness checking on the keys of a given input object, to make sure, say, all the elements of a ZodUnion appear in the object. That's why Zod makes all fields optional for literal keys.
Exactly, this is the goal. For maximum type safety, Zod should add It's important to note that Zod's isn't able to alter its runtime behavior based on your Let's talk about improvements I'd like to make to
|
Because of a quirk of the extends syntax of typescript conditional
types, the previous version of this check for a branded key didn't work
as expected.
Thank you @akomm for the explanation of why the extends sides need to be
swapped at #2287 (comment)
Originally I intended to remove the RecordType type entirely, since it
would seem initially that it is only there to work around earlier
versions of typescript not having the
noUncheckedIndexedAccess
option.However, it is really meant to work around a problem with using ZodEnum
as the key, see this comment: #2287 (comment)
The RecordType conditional type seems to exist for handling the partialness of indexed objects. However that is handled by the tsconfig option noUncheckedIndexedAccess in [email protected] and later.