HN Debrief

Parse, Don't Validate – In a Language That Doesn't Want You To

  • Programming
  • Developer Tools
  • Open Source

The post takes the old “parse, don’t validate” idea from functional programming and shows how to approximate it in TypeScript. Instead of checking raw strings and booleans over and over, you parse them once into domain-specific types like Email or VerifiedUser and then write the rest of the code against those narrower types. In TypeScript that usually means branded types, discriminated unions, parser functions, and runtime schema tools like Zod, because the language is structurally typed and erases types at runtime. Several people liked the framing because it pushes validation to the boundary where data enters the system, then lets the compiler enforce cleaner invariants inside the app.

If you run a TypeScript codebase, the practical move is to parse external data at boundaries and carry richer types inward only where the business risk justifies it. Avoid trying to force the whole app into a Haskell-shaped model unless your team is willing to buy into heavier tooling like Effect or another language entirely.

Discussion mood

Mostly positive on the core idea, but firmly pragmatic. People liked parsing at boundaries and richer domain types for risky inputs, while rejecting attempts to push TypeScript into full functional-programming rigor or an explosion of micro-types.

Key insights

  1. 01

    JSON Schema is the competing path

    Instead of making Zod the source of truth, some teams define normal TypeScript types, generate JSON Schema at build time with tools like typescript-json-schema, and validate production payloads with Ajv. That keeps the runtime dependency surface small, fits OpenAPI better, and avoids tying your domain model to a specific validation library’s syntax. The tradeoff is obvious and real. Once you need constraints that TypeScript cannot express, you end up stuffing semantics into annotations or comments, which is exactly the kind of split source of truth Zod was built to avoid.

    If you already publish OpenAPI or rely on JSON Schema across services, schema generation plus Ajv is a coherent stack. If your app needs heavy transformations and runtime-first parsing ergonomics, Zod or a similar parser library will usually be the faster path.

      Attribution:
    • Xenoamorphous #1 #2
    • sheept #1
    • gabes #1
    • BiteCode_dev #1
  2. 02

    Optional fields are not the hard case

    The scary example of type explosion mostly disappears once you separate two cases. A field like `phone: ValidPhoneNumber | null` is fine when absence is a legitimate state you must handle everywhere. You only need separate refined types when multiple fields are correlated and together represent a business state, which is better modeled as a discriminated union or a small parsed sub-object than as every possible mix of optional flags. That is also where refinement types would help in stronger languages, but in mainstream TypeScript the advice was blunt. Parse the expensive boundaries and stop there.

    Do not create a new interface for every optional-property combination. Reserve new types for state transitions and correlated invariants, then leave simple absence as `null` or `undefined` with a validated inner type.

      Attribution:
    • xx_ns #1
    • columnarx3 #1
    • sirwhinesalot #1 #2
  3. 03

    TypeScript already gives safer alternatives to casts

    Several comments zoomed in on how often developers reach for `as` when they really want either compile-time conformance checks or runtime narrowing. `satisfies` can often replace assertion-style casts when you want the compiler to verify a value matches a shape without changing its inferred type. Type guards handle the runtime case by proving a narrower type after a check. The larger point was that frustration with TypeScript often comes from using `as` as an escape hatch, then blaming the language for the holes that creates.

    Audit your codebase for `as` casts before adding more schema machinery. Replacing a chunk of them with `satisfies`, type guards, and boundary parsers will buy safety faster than another abstraction layer.

      Attribution:
    • jerf #1 #2
    • SebastianKra #1
    • chrisfarms #1
    • WorldMaker #1
  4. 04

    Effect solves more, but demands buy-in

    Effect came up as the version of this idea for teams that want typed error channels, resource management, concurrency, workflows, and richer schemas all under one model. People using it argued that its Schema layer is stronger than Zod and can encode stateful guarantees that plain branded strings cannot. The catch is that it wants to shape the architecture around itself, which makes it powerful for complex systems and overkill for ordinary CRUD apps. Even advocates said they would not recommend it without a strong internal champion and a real complexity problem to justify the cost.

    Treat Effect as an architecture choice, not a drop-in validation library. It can pay off for workflow-heavy systems, but it is the wrong answer if you just need safer request parsing in a typical web app.

      Attribution:
    • epolanski #1 #2 #3
    • programmarchy #1
    • steve_adams_86 #1
    • cptmurphy #1
  5. 05

    Zod at the edge is the practical compromise

    The strongest applied advice was to use Zod or an equivalent parser exactly where untrusted data enters the system, then keep the rest of the app close to ordinary TypeScript. That preserves the biggest win of “parse, don’t validate” without fighting the ecosystem’s expectation of exceptions, plain objects, and simple return types. It also acknowledges a social reality. If the type discipline becomes too clever, teammates will route around it and the safety story collapses.

    Standardize boundary parsing for requests, database reads, env vars, and URLs. Keep internal APIs boring unless the extra type precision blocks a class of bugs you actually see.

      Attribution:
    • gherkinnn #1
    • Altern4tiveAcc #1
    • ramon156 #1
  6. 06

    Exhaustive checks should still fail loudly

    One concrete code-style point stood out. The common TypeScript exhaustiveness pattern that assigns to `never` and then returns the value catches missing cases at compile time, but it produces nonsense behavior if an invalid value slips through at runtime. Throwing in the default branch keeps the compiler check and gives you a real failure mode when the world diverges from your assumptions. In a language with erased types and lots of external data, that runtime backstop is not optional.

    Keep the `never` exhaustiveness check, but make the default branch throw. Compile-time certainty in TypeScript does not remove the need to fail safely when runtime inputs go off-script.

      Attribution:
    • whilenot-dev #1 #2
    • terminatornet3 #1

Against the grain

  1. 01

    Use another language if you want stronger guarantees

    A credible minority view was that TypeScript is simply the wrong substrate for this style once you care deeply about nominal types, constructor hiding, or stronger compile-time guarantees. From that angle, branded types and parser combinators are clever patchwork for a language whose runtime model will never really cooperate. F#, Haskell, OCaml, or Rust compiling to JavaScript were presented as cleaner choices if your team genuinely wants those semantics instead of an approximation.

    If your product genuinely depends on deep type-level guarantees, compare the cost of moving critical code to a stronger language against the ongoing cost of emulating those guarantees in TypeScript.

      Attribution:
    • gherkinnn #1
    • botfriendsarent #1
    • exceptione #1
    • toolslive #1
  2. 02

    Branding can overpromise what was actually checked

    Some people objected that a branded primitive often records only that a validator once said “yes,” not that the value’s structure now makes misuse impossible. That weakens the rhetorical force of “parse, don’t validate” because the code may still rely on unstated assumptions that the type itself does not encode. For domains where downstream logic depends on parts of the data, a real parsed structure can be the more honest representation.

    When later code needs components or invariants, parse into a structured object instead of just tagging a string. A branded primitive is best for identity and boundary hygiene, not for carrying rich semantics by implication alone.

      Attribution:
    • throwaw12 #1
    • wwalexander #1 #2

In plain english

Ajv
Another JSON Schema Validator, a popular JavaScript library for validating data against JSON Schema definitions.
branded types
A TypeScript pattern that adds a fake marker to an existing type like string so it behaves as a distinct type to the compiler.
CRUD
Create, Read, Update, Delete, the basic set of operations common in database-backed business applications.
discriminated union
A type made of several object variants that share a tag field, letting code safely branch on the tag to know which variant it has.
Effect
A TypeScript library and programming model that combines typed effects, error handling, dependency management, concurrency, and schema-based parsing.
JSON Schema
A standard format for describing the structure and constraints of JSON data so software can validate it.
OpenAPI
A standard way to describe web APIs, including their endpoints, inputs, outputs, and schemas.
refinement types
Types that include extra logical conditions, such as a number being positive or a user having a non-null birthday.
satisfies
A TypeScript keyword that checks a value conforms to a type without forcing the value to become that type.
TypeScript
A typed superset of JavaScript that adds compile-time type checking but usually removes those types when code runs.
Zod
A TypeScript-first schema and validation library used to parse and validate runtime data into typed values.

Reference links

Type systems and language tooling

  • Fable
    Mentioned as a way to write F# and compile to JavaScript instead of forcing functional patterns into TypeScript.
  • TypeScript handbook on exhaustiveness checking
    Cited in the argument over the standard `never` pattern for exhaustive switch checks.
  • TypeScript Playground example
    Shared as a concrete example of deriving refined user types with TypeScript utility types and type guards.
  • ts2ocaml
    Given as an example of bridging OCaml and TypeScript through interop rather than staying purely in TS.

Talks and essays mentioned in comments