ArkType: The Parse-Don't-Validate Sequel I Didn't Know I Needed · cekrem.github.ioTwo days ago I published a post about parsing instead of validating in TypeScript. I hand-rolled branded types with unique symbol, wrote Parsed result types, and stitched together parsers with early returns. It worked. It was also kind of ugly, and I said so at the time.<br>Then someone in the Reddit comments linked me to ArkType.<br>I’d heard the name before but never actually sat down with it. After a few hours of poking around I have opinions. Some of them are strong. A couple might even be wrong (I haven’t used this in production yet, so grain of salt and all that). But I think ArkType is doing something interesting for TypeScript, and it lands squarely on the parse-don’t-validate thread I’ve been pulling at.<br>Quick recap (or: go read the other post)
Link to heading<br>The short version of the previous post: a validator checks data and throws away what it learned. A parser checks data and encodes what it learned in the type. Branded types let you fake nominal typing in TypeScript’s structural system, and the parser is the one trusted boundary where you’re allowed to as Brand your way past the compiler.<br>The long version has code examples. Go read it if you haven’t. I’ll wait.<br>The pain point I ended on, though: hand-rolling branded parsers in TypeScript is verbose, repetitive, and requires discipline to keep the as casts contained. I mentioned Zod as the ergonomic answer, but noted that even Zod doesn’t change the mindset problem. You still have to choose to use it.<br>So where does ArkType fit?<br>First contact
Link to heading<br>ArkType’s pitch is “TypeScript’s 1:1 validator, optimized from editor to runtime.” Let me just show you:<br>import { type } from "arktype";
const User = type({<br>name: "string",<br>email: "string.email",<br>age: "0 ,<br>});
I stared at that for a while. The strings are the types. "string.email" isn’t a method chain or a function call. It’s a string literal that ArkType’s compiler parses into both a TypeScript type and a runtime validator. And the range constraint on age? Also a string. "0 . Reads like a type annotation you’d wish TypeScript had natively.<br>The TypeScript type falls out automatically:<br>// { name: string; email: string; age: number }<br>type User = typeof User.infer;
No z.infer. No separate type definition to keep in sync. You write the thing once and both sides (compile-time and runtime) agree on what it means.<br>If you’ve read Scott Wlaschin’s Domain Modeling Made Functional, you’ll recognize what’s happening here: make illegal states unrepresentable. In Elm I’d reach for an opaque type and a smart constructor, and in F# you’d use a single-case discriminated union. TypeScript makes you fight for it, which I spent most of the previous post complaining about. ArkType picks that fight for you. A string tells you nothing. An Email tells you something. A type({ email: "string.email" }) tells you something and enforces it at runtime. That’s the bit that’s hard to get in TypeScript without a library doing the heavy lifting.<br>The parsing story
Link to heading<br>So what does it actually look like when you use this thing?<br>const out = User(rawData);
if (out instanceof type.errors) {<br>console.error(out.summary);<br>// "email must be an email address (was 'not-an-email')"<br>// "age must be at most 150 (was 200)"<br>return;
// out is fully typed as { name: string; email: string; age: number }<br>out.name;
This is parsing. Raw data goes in, typed data or errors come out. The caller has to handle both branches before touching the result. Same job as my hand-rolled Parsed from the previous post.<br>But I have a gripe.<br>That error check uses instanceof. Not a discriminated union. Not a kind field. instanceof. I get why they did it (a Result wrapper means allocating { ok: true, data: T } on every successful validation, and when you’re targeting 14-nanosecond validation that allocation actually matters), but it still feels wrong. A discriminated union says “this value is one of two things” in the type itself. instanceof says “go check the prototype chain.” Those aren’t the same thing, and if you’ve spent any time in Elm or F# you’ll feel that friction immediately.<br>It also means ArkType’s output isn’t composable with the FP ecosystem (neverthrow, Effect, fp-ts) without wrapping it yourself. You want to pipe the result into a Result-based pipeline? Write an adapter. It works, but it’s the kind of thing that makes me sigh quietly. (To be fair, I haven’t checked whether someone’s already published an adapter package. Probably someone has. The npm ecosystem is nothing if not thorough.)<br>Where it gets interesting: morphs
Link to heading<br>Okay, this is the part that actually got me excited. Morphs are ArkType’s version of...