-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
[POC] [runtime performance] bottom up lazy issues/paths construction #3487
Conversation
15782aa
to
144a2dc
Compare
This is really great stuff. I actually have this exact kind of change implemented in a The inability to support In my branch I've taken care to lazily initialize I plan to clean it up my branch and merge into As a aside there's an issue with this exact approach that is quite subtle and took me a while to figure out. This line in ZodEffects#_parse can cause issues: const value = inner.status === "valid" ? inner.value : input; You can't assume z
.string()
.transform((val) => val.length)
.refine(() => false, { message: "always fails" })
.refine(
(val) => {
console.log(`val`, val, typeof val);
return (val ^ 2) > 10;
} // should be number but it's a string
)
.parse("hello") To get around this you need some way to attach data to an |
That's awesome to hear! 🔥 Are you also addressing
I actually thought about this, but forgot about it at some point 😂
I tried this as well, but it didn't make any meaningful difference in my tests 🤔 |
Indeed, I was planning to do promise detection just as you implemented in your branch, but I haven't done it yet. The goal is to detect async refinements/transforms and reflect that in the inferred type. This would then "bubble up" the schema — a ZodObject containing a With this + promise detection, Zod could provide a single unified method that obviates the
IIRC I think it only makes a significant difference on primitive parsing. There's already enough overhead in ZodObject that the impact is minimal. But I'll keep experimenting. |
So I'm gonna work off of this branch after all 😅 My Re: putting |
daf4da1
to
776019a
Compare
Awesome! I won't be able to work on this in the upcoming days so in case you're actively going to be pushing to this branch, I went ahead and fixed the rebase conflicts between this branch and the promise detection branch, so feel free to cherry-pick this commit onto it if you like: jussisaurio@0f20140 Fair warning, there's a few test failures in it (because I don't unfortunately have time to fix them) |
@jussisaurio Awesome, just cherrypicked & fixed the tests. Gonna merge this so I can start putting up more PRs against these changes. Try not to rewrite Zod again in the next few days — I'm switching Zod to a monorepo so everything will be moving 🙃 |
Hi @colinhacks ,
I made a little POC targeting Zod's runtime performance with a similar approach that has been done in other parse-dont-validate style libraries where issue paths are only allocated when they are needed (i.e. when there actually are issues in parsing). This means paths don't need to be allocated in any successful parse cases, which is the common/hot path generally.
I'm not sure if this fits at all with your plans for the
v4
version, but I did this mostly as an exercise, and if a polished version of it ends up being something that's feasible from your POV, then great!Main changes:
_parse()
now returns eitherOK(validResult)
orINVALID(issues: IssueData[])
; i.e. issues are not added toctx
but are returned along with the unsuccessful status value. Parent schemas will append the child issues to their own issues array recursivelypath
to the child issues, i.e. if an error happens in keyfoo
of an object, the parent will addpath: ["foo"]
to the child error. This happens recursively, so the parent's parent will then unshift e.g.path: ["bar", "foo"]
later. As mentioned, this happens on demand so paths are only constructed when the parser encounters an error.ParseContext
objects are allocated during parsing inside_parse()
since they are not necessaryParseStatus
and the concept ofdirty
are removed; dirtiness is handled separately inZodEffects
to decide whether to continue with refinements. LMK if I've misunderstood anddirty
is needed elsewhereinput
is added toIssueData
to continue supporting error maps operating on the parse inputctx.addIssue()
is not used in the internals, refinements continue to support thectx.addIssue()
API, which just pushes to theissues
array under the hoodSome caveats / edge cases / breaking things:
async
parsing, refinements and handling some edge cases like prototype pollution etc etc) because I mainly wanted to get a POC done where the test set passes and I could concretely demonstrate the performance difference"use path in refinement context"
, since paths are now on-demand constructed, and so, not available to the refinement context. This would be a breaking change ofc, but I couldn't figure out how to make that backwards compatiblePerformance:
Here's a performance comparison using the typescript-runtime-type-benchmarks library:
colinhacks/v4:
jussisaurio/v4-lazy-issues: