DEV Community

Cover image for I Compared 6 TypeScript Validation Libraries. Here's What Surprised Me 🤔

I Compared 6 TypeScript Validation Libraries. Here's What Surprised Me 🤔

nyaomaru on October 28, 2026

Hoi hoi! 👋   I'm nyaomaru, a frontend engineer and OSS developer working mostly with TypeScript. When we need runtime validation in TypeScri...
Collapse
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

bro how you create these things hahahaha 😆🤣🤣🤣
you are SERIOUS DEV + FUNNY GUY 😆
by the way nice writing and your cover image 🫡

Collapse
 
nyaomaru profile image
nyaomaru •

It’s my pleasure! 😸

I’m a funny frontend engineer!! 😹

And yep, I draw all my cover images by hand! 😹😹😹

Picked as gem
Collapse
 
pengeszikra profile image
Peter Vivo •

Draw by hand ....... strange!

Collapse
 
puneetkumar2010 profile image
Puneet Kumar Mishra •

Drawing by hands!!!!
My respect for nyaomaru day by day: 📈📈📈📈📈📈📈
lol...

Thread Thread
 
akhourianmolkumar profile image
Akhouri Anmol Kumar •

7year plus experience too...

Thread Thread
 
nyaomaru profile image
nyaomaru •

Haha, thank you!! 😹😹😹
My drawing skill is slowly leveling up too! 📈🎨😸

Collapse
 
pengeszikra profile image
Peter Vivo •

This comparison with examples is very useful—thank you! I’ve saved the post it seems super informative, will be a help in deciding which one to choose!

Collapse
 
nyaomaru profile image
nyaomaru •

I’m really happy the comparison was useful enough to save.

I hope it helps when you’re deciding which approach fits your project best! 🙌

Collapse
 
shubhradev profile image
Shubhra Pokhariya •

Nice comparison, and JavaScript as the Magikarp on the cover got me 😄 The emoji test is the one I’d check against my database, since Postgres varchar(100) counts characters while JavaScript’s string.length counts UTF-16 code units, so the validator and column can disagree on the same string.

Collapse
 
nyaomaru profile image
nyaomaru •

Haha, I’m glad you noticed the Magikarp 😹 Because it’s full of potential! 😹

And yes, that Postgres example is a perfect case where the validator and database can both be “correct” but still disagree on the same string. 👀

Collapse
 
mohsen_jr profile image
Mohsen jr •

The build setup is an interesting trade-off with typia. Saving runtime overhead is great, but in a team project, slower builds and extra tooling can affect the whole development workflow, not just validation speed.

Collapse
 
nyaomaru profile image
nyaomaru •

Yeah, that’s the trade-off 😸

typia’s runtime performance is impressive, but the build setup becomes part of the cost too.

In a team project, that affects everyone’s workflow, not just the validation path. 😺

Collapse
 
stackmasteraliza profile image
Aliza Ali •

"Ask what the boundary needs to return" is the right first question. Two things worth adding: Zod, Valibot and ArkType all implement Standard Schema now, so tools that accept it (tRPC, TanStack Form and others) don't lock you into one validator, and you can genuinely pick per boundary. And the ArkType precompile point matters in practice: under a strict CSP without unsafe-eval, or on runtimes that block new Function, you're on the fallback path, so it's worth benchmarking in the environment you actually deploy to. Great comparison!

Collapse
 
nyaomaru profile image
nyaomaru •

Really good additions 😸

Standard Schema definitely makes the “pick per boundary” idea much more practical.

And yes, the ArkType CSP / new Function point is important too. Benchmarking the actual deployment path matters more than the ideal fast path.

Thanks for adding this! 🙌

Collapse
 
xulingfeng profile image
xulingfeng •

Full honesty, that cover art hooked me immediately, haha. I love Pokémon so much. Yellow, Gold and Silver are timeless; the GB era is still the best. The Switch entries feel like they lost a little spark somehow. My beloved Pikachu looks so weird these days, lol 😂

Collapse
 
nyaomaru profile image
nyaomaru •

Haha, same here 😹

I really love Red / Green / Gold / Silver too. That GB era has such a special feeling.

Pikachu definitely looked cuter back then too 😹

Collapse
 
xulingfeng profile image
xulingfeng •

Right?! The pixel‑art vibe is irreplaceable. Nothing beats GB‑era Pokémon 🎮

Thread Thread
 
nyaomaru profile image
nyaomaru •

Pi-pi ka chu~! 😸⚡🎮

Thread Thread
 
xulingfeng profile image
xulingfeng •

hahaha🤣🤣🤣

Collapse
 
idanshalem profile image
Idan Shalem •

For a React form, this suggests keeping the input counter and schema on one shared counting rule, rather than mixing value.length, HTML maxLength, and schema validation.

For code points, matching Zod 4.6.5 in your comparison:

const count = (value: string) => [...value].length;

The component can display count(value)/100 and show the validation error without truncating the controlled value in onChange, which can interfere with IME composition.

If the product means user-perceived characters instead, use Intl.Segmenter with granularity: 'grapheme' for the counter and the schema's custom refinement. Otherwise a family emoji can still count differently in the UI and validation.

Collapse
 
nyaomaru profile image
nyaomaru •

Good point! 😸

I especially like the idea of sharing the same counting rule between the counter and the schema. Otherwise length, maxLength, and validation can quietly mean different things.

And the IME point is important too. For Japanese, Chinese, Korean, and other composition-based input, I’d rather avoid truncating the controlled value inside onChange.

Intl.Segmenter is also a nice reminder that even “character count” needs a definition. 😹

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗵𝗻 •

"The hardest part was not making seven validators compile. It was making seven validators mean the same thing." - this hits the nail on the head! 😸

Most benchmarks just throw a basic object at each library without aligning the actual runtime contract—like whether extra keys are stripped or rejected, how UTF-16 code units vs. code points are counted, or what happens with non-JSON edge cases like inherited properties and sparse arrays.

I also really like framing the choice around what you actually need at the boundary: if you just need a fast boolean narrowing check (isUser(input)), pulling in a full parsing and error-formatting schema engine is overkill, whereas form validation or public APIs genuinely need those rich error trees. Great, super thorough comparison, nyaomaru! 🚀

Collapse
 
nyaomaru profile image
nyaomaru •

Thank you so much, John! 😸

That’s exactly what I wanted the comparison to show. 👍

And yes, I think the boundary should decide the tool.

If all I need is a boolean narrowing check, I’d rather keep it small.
If I need rich errors, parsing, transforms, or integrations, then a schema system starts earning its complexity.

Really glad that point came through! 🚀

Collapse
 
emilywoodsnyc profile image
Emily Woods •

The schema with transforms caveat is the bit I’d turn into a separate fixture. I’d feed in a value that parses successfully but changes type, then check that the original input is still kept separate from the parsed output. It’s easy to focus on which values get rejected and miss what a successful parse actually returns.

Collapse
 
nyaomaru profile image
nyaomaru •

Ahh, I see 😸

That’s a good point. I was mostly looking at accepted/rejected behavior, but what comes back after a successful parse matters too.

Definitely something worth checking separately. 👀

Collapse
 
alexshev profile image
Alex Shev •

Matching the runtime contract before comparing speed is important. UTF-16 length, unknown keys and coercion are easy places for validators to diverge while appearing to accept the same TypeScript type.

Collapse
 
nyaomaru profile image
nyaomaru •

Exactly 😸

If the runtime contract isn’t aligned first, a speed comparison can end up measuring different behavior.

UTF-16 length, unknown keys, and coercion are all perfect examples of small semantic differences that can completely change the result. 👍

Collapse
 
hordii_bohdan profile image
Bohdan Hordii •

Synthetic benchmarks rarely expose the real production bottleneck: the aggressive GC pressure triggered by recursive object allocation and unknown-key stripping on high-throughput ingest paths. While AOT-compiled approaches crush runtime parsers on raw ops/sec, you trade that speed for brittle build pipelines and restrictions inside hardened, zero-eval runtime environments. At 50k req/s, are you measuring schema parsing efficiency, or just watching V8 memory churn choke your event loop?

Bohdan Hordii // bohdanhordii.dev

Collapse
 
nyaomaru profile image
nyaomaru •

That’s a fair criticism 😸

My benchmark is only measuring steady-state validation cost, not GC pressure or 50k req/s ingest behavior.

At that scale, allocation patterns, stripping/cloning behavior, and event-loop pressure could matter more than raw validator ops/sec.

And yes, AOT speed comes with a very real trade-off in build complexity and runtime constraints.

Collapse
 
mudassirworks profile image
Mudassir Khan •

the line about making seven validators mean the same thing is the real finding. the ArkType row stood out to me, 291 ns on the valid path but 9.6 ns on a wrong id, so rejection is almost free and the success path pays for it. that changes which one fits a hot boundary like webhooks, where most junk dies at the first field.

one gotcha i would test next is the exact keys rule on payloads that grow over time, since an exact object turns every new optional field from a producer into a hard failure.

did you try a strip mode on any of them, or only exact?

Collapse
 
nyaomaru profile image
nyaomaru •

Good catch 😸

That ArkType result is interesting, although I’d be careful treating 9.6 ns as “rejection cost” in general.

That fixture fails very early on the id, so it’s really showing how cheap an early exit can be.

And yes, exactness can become a compatibility decision too. A producer adding a harmless field can suddenly break a consumer.

For this comparison I focused on accept-vs-reject semantics and exact mode. I didn’t benchmark a strip mode separately.

That would be a really interesting follow-up, especially because then the contract isn’t just “is this valid?” but also “what value comes back?” 👀

Collapse
 
luke_bennett_ed52405c798e profile image
Luke Bennett •

No not yet x,

Collapse
 
sherotree profile image
Sherotree •

The probe table is the part I'd actually keep next to the 245/245 result. All seven agree on the JSON fixtures, then split on a sparse tags array and on an extra key set to undefined: typia accepts that undefined key while Zod, Valibot, and the handwritten guard reject it. Exact only means enumerable string keys on ordinary JSON, which is fine for a request body and a hole if the same guard also sees objects built in process.

Collapse
 
nyaomaru profile image
nyaomaru •

That’s a good way to frame it 😸

The 245/245 result shows agreement on the JSON contract, while the probe table shows where that agreement stops.

The extra: undefined case is especially interesting because typia accepts it while several others reject it.

So yes, I think keeping the probe table right next to the main result makes the boundary of “exact” much clearer. 👀

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

Making seven validators mean the same thing is the real finding here. The Zod length check using code points instead of UTF-16 units is the kind of mismatch that only shows up in fixtures.

Collapse
 
nyaomaru profile image
nyaomaru •

Exactly 😸

That Zod case was one of the best examples for me.

On paper the rule looked identical, but the fixture exposed a different runtime contract immediately.

That’s why I ended up trusting the shared fixtures more than the schema syntax. 👀

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

That emoji fixture is the part that sticks. 51 emoji passing one length check and failing another is a clean way to show two schemas can look identical and still disagree.

Collapse
 
technogamerz profile image
𝐓𝐡𝐞 𝐋𝐚𝐳𝐲 𝐆𝐢𝐫𝐥 •

The cover image is so good, wow! Op🔥🔥
I have no words. By the way, my entire focus was on Unknown. How long did it take you to make this? I mean, creating something like this in Photoshop with all those layers must take a lot of time.

Collapse
 
nyaomaru profile image
nyaomaru •

Thank you so much! 😸🔥

Yeah, it takes quite a while, especially with all the layers. 😹

But I usually make these cover images as a little break, so I actually enjoy the process!

Collapse
 
peter7865 profile image
Peter •

Type safety on data boundaries is tricky. At The Printing World, we hit this when handling automated print job specs coming from external clients—one mismatch and a whole batch gets misaligned. Good breakdown!

Collapse
 
nyaomaru profile image
nyaomaru •

That’s a perfect real-world example 😸

At a boundary like that, one “almost correct” payload can still cause a very real production problem.

That’s why I like treating external data as unknown until the runtime contract is actually proven. 👀

Collapse
 
shikha_shukla_2469c41a94e profile image
Shikha Shukla •

new learning for me

Collapse
 
nyaomaru profile image
nyaomaru •

I'm happy to hear that! 😸

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala •

The artwork in the cover banner is so cool! Loved it 😀

Collapse
 
nyaomaru profile image
nyaomaru •

Thx!! 😸
Honestly, making the cover image probably takes me the most time. 😹

Collapse
 
alexshev profile image
Alex Shev •

Comparisons like this are most valuable when they show where the schema becomes a boundary rather than a convenience. I would watch for three costs separately: developer ergonomics, runtime failure clarity, and how safely validation composes across API and background-job entry points.