Commit 4e1720c
authored
fix(v4): align record keys and intersection strictness with TypeScript (#6412)
* fix(record): classify an out-of-set key by whether the key schema is enumerable
An enumerable key schema (z.enum, z.literal([...])) declares which keys the
record owns, so a key outside that set is unrecognized. A non-enumerable one
(regex, refine) is a constraint every key must satisfy, so a failing key is
invalid. Only the first is reconcilable against the other side of an
intersection, which handleIntersectionResults already does for
unrecognized_keys.
The branch that emitted the issue keyed off def.partial rather than the key
schema, so partialRecord(z.enum([...])) reported invalid_key and its keys were
never reconciled -- the object side of an intersection could not claim its own
keys. Fixes #2200, #2573.
* fix(record): a record's key schema governs its own keys, not every key
A record's key schema declares which keys that record is responsible for. It
is not a predicate every key in the result must satisfy. TypeScript agrees:
in `{name: string} & Record<\`S_\${string}\`, string>` the index signature
constrains only the keys matching it, so `name` is accepted, while a matching
key with the wrong value type is still an error.
Zod's inferred types already match TypeScript here, but the runtime rejected
the object's own keys with invalid_key, so z.infer accepted values the parser
threw on. Two changes:
- partialRecord with an enumerable key schema now reports an out-of-set key as
unrecognized_keys, like z.record(z.enum([...])) already did. The branch keyed
off def.partial rather than off the key schema.
- handleIntersectionResults reconciles a record's invalid_key the same way it
already reconciles unrecognized_keys, so a key one side does not govern is
reported only when neither side governs it.
Standalone records are unchanged: a key outside the key schema is still an
error, matching TypeScript's excess-property check on a fresh literal.
Fixes #2200, #2573.
* fix(v4): let an unrecognized key continue, so operands run their own logic
unrecognized_keys describes the shape of the input, not the validity of the
parsed value, so it no longer aborts the schema it came from. A strict operand
inside an intersection now runs its own checks, refinements and transforms, a
nested default still applies, and an extra key no longer suppresses the other
errors on the same object. The parse still fails.
A pipe keeps its rule that any issue stops it, so a failing refinement never
feeds its transform -- the one exception is unrecognized_keys, where the value
handed downstream is identical whether or not the extra key was present.1 parent 4cc4053 commit 4e1720c
3 files changed
Lines changed: 182 additions & 38 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |