ArkType: The Parse-Don't-Validate Sequel I Didn't Know I Needed

jcbhmr1 pts0 comments

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&rsquo;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&rsquo;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&rsquo;t-validate thread I&rsquo;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&rsquo;s structural system, and the parser is the one trusted boundary where you&rsquo;re allowed to as Brand your way past the compiler.<br>The long version has code examples. Go read it if you haven&rsquo;t. I&rsquo;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&rsquo;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&rsquo;s pitch is &ldquo;TypeScript&rsquo;s 1:1 validator, optimized from editor to runtime.&rdquo; 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&rsquo;t a method chain or a function call. It&rsquo;s a string literal that ArkType&rsquo;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&rsquo;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&rsquo;ve read Scott Wlaschin&rsquo;s Domain Modeling Made Functional, you&rsquo;ll recognize what&rsquo;s happening here: make illegal states unrepresentable. In Elm I&rsquo;d reach for an opaque type and a smart constructor, and in F# you&rsquo;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&rsquo;s the bit that&rsquo;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&rsquo;re targeting 14-nanosecond validation that allocation actually matters), but it still feels wrong. A discriminated union says &ldquo;this value is one of two things&rdquo; in the type itself. instanceof says &ldquo;go check the prototype chain.&rdquo; Those aren&rsquo;t the same thing, and if you&rsquo;ve spent any time in Elm or F# you&rsquo;ll feel that friction immediately.<br>It also means ArkType&rsquo;s output isn&rsquo;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&rsquo;s the kind of thing that makes me sigh quietly. (To be fair, I haven&rsquo;t checked whether someone&rsquo;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&rsquo;s version of...

rsquo type arktype string email typescript

Related Articles