Hacker News new | ask | show | jobs
by nayajunimesh 306 days ago
Most validation libraries like Zod create deep clones of your data during validation, which can impact performance in high-throughput applications. I built decode-kit to take a different approach: assertion-based validation that validates and narrows TypeScript types in-place, without any copying or transformation. Here's what the API looks like in practice:

import { object, string, number, validate } from "decode-kit";

// Example of untrusted data (e.g., from an API) const input: unknown = { id: 123, name: "Alice" };

// Validate the data (throws if validation fails) validate(input, object({ id: number(), name: string() }));

// `input` is now typed as { id: number; name: string } console.log(input.id, input.name);

When validation fails, decode-kit takes an equally thoughtful approach. Rather than being prescriptive about error formatting, it exposes a structured error system with an AST-like path that precisely indicates where validation failed. It does include a sensible default error message for debugging, but you can also traverse the error path to build whatever error handling approach fits your application - from simple logging to sophisticated user-facing messages.

The library also follows a fail-fast approach, immediately throwing when validation fails, which provides both better performance and clearer error messages by focusing on the first issue encountered.

I'd love to hear your thoughts and feedback on this approach.

1 comments

> fail-fast approach, immediately throwing when validation fails

would this mask any errors that would occur later in the validation?

With the fail-fast approach, yes - unless we introduce an option to collect all errors. In my own applications, I have found this to be a better default because the 'average' requests is valid and paying a constant overhead just to be thorough on rare invalid cases can be wasteful.

My overall takeaway has mostly been to not optimize for the worst case by default. Keep fail-fast as baseline for boundaries and hot paths, and selectively enable “collect all” where it demonstrably saves human time.

A useful counterexample, where one wants to see all errors, is validating form input. But it's very much ok to say your library is not meant for that use case!
That is a good point! How do you feel about fail-fast by default but being able to configure a validator to collect all errors (like Zod does by default)? Something like array(number(), { failFast: false });

We currently expose error as a tree structure so that it's easy to map an error to a value or build custom error messages (we only provide debug error message) and I haven't been able to come up with a satisfactory error API that accommodates multiple error paths, but you raise an excellent point. Thanks for pointing out.

My instinct is to say keep it small, simple & focused.