Update: If you liked this post, the follow-up — Effect Without Effect-TS: Algebraic Thinking in Plain TypeScript — picks up where we left off and takes the ideas further.
I’ve been thinking about Alexis King’s Parse, don’t validate again. I do this quite regularly, actually, usually after staring at a TypeScript codebase that’s been quietly accumulating if (user.email) checks like barnacles. The post is from 2019, and the advice (or rather principle) is way older than that. And yet most TypeScript I read — including, embarrassingly, plenty I’ve written — still validates instead of parsing.
The pitch, if you haven’t read it (you should): a validator says “this thing is fine, please continue.” A parser says “give me a blob, and I’ll either give you back a more precise type or tell you why I can’t.” The difference sounds academic until you realize that validators throw away information the moment they finish running, while parsers preserve what they learned by encoding it in the type. Once you’ve parsed a string into an EmailAddress , the rest of your program never has to wonder again. Peace of mind and more mental capacity for the fun stuff.
In Haskell or Elm or F# this is just how you write code. The language pulls you toward it. In TypeScript… it doesn’t. TypeScript will happily let you do the right thing, but it won’t insist, and it won’t even gently nudge. If anything, structural typing actively undermines the whole game.
Let me show you what I mean.
The validator we’ve all written Link to heading
Here’s the kind of code I see (and write) constantly:
interface User { id: number ; email: string ; age: number ; } // The actual validation is naîve and simplistic, but you get the point: function isValidUser(user: User ) : boolean { if ( ! user.email.includes( "@" )) return false ; if (user.age < 0 || user.age > 150 ) return false ; return true ; } function sendWelcome(user: User ) { if ( ! isValidUser(user)) { throw new Error ( "invalid user" ); } // ...later, deeper in the call stack: emailService.send(user.email, `Welcome, age ${ user.age } ` ); }
Spot the lie? User.email is just string . User.age is just number . The validation happened — congrats — but the type system forgot about it the instant isValidUser returned. Three function calls deeper, when somebody touches user.email , there is nothing stopping them from passing it to a function that expects a real email. Because as far as TypeScript is concerned, it’s just a string. Same as "" , same as "hello" , same as "definitely not an email" .
So what do we do? We re-validate. We add another if . We write a unit test. We hope. (King has a much better word for this in the original post: “shotgun parsing” — validation scattered everywhere, none of it remembered.)
... continue reading