Prufy notes
Types and data ยท 10 min

Types that prevent bugs

All of these techniques share one goal: turn a mistake you could make at run time into something that doesn't compile.

You'll recognise most of these individually. What's worth noticing is how they combine: by the time a value reaches the logic, most wrong programs can't even be written.

Loose versus strictPick a kind of bug
Compiles, and it's nonsense
type Claim = {
  status: string
  verifiedAt?: Date
  expiredAt?: Date
}

// Verified, but when? And also expired?
const c: Claim = {
  status: "verified",
  expiredAt: new Date(),
}
Doesn't compile
type ClaimState =
  | { status: "verified"; verifiedAt: Date; lastSeenAt: Date }
  | { status: "expired"; expiredAt: Date }

const c: ClaimState = {
  status: "verified",
  expiredAt: new Date(),
}
packages/core/src/claim.ts
One object with optional fields allows every combination, and most of them are nonsense.

One source for every closed set

One list, three uses
const CLAIM_STATUSES = ["pending", "verified", ...] as const
The type
type ClaimStatus
Checked by the compiler.
The validator
z.enum(CLAIM_STATUSES)
Checked at run time, on what comes in from outside.
The database
pgEnum("claim_status", ...)
Checked by Postgres, on every write.
Add a status to the one array, and the compiler walks you to every switch that needs it. The migration generator picks up the database side.

Two doors for a domain

An AsciiDomain can only come from two places, and the difference matters.

normalizeDomain
For new input. Applies policy, including the public suffix list.
rehydrateAsciiDomain
For rows we stored ourselves. Checks only the syntax.

Trust stops at the edges

Types disappear at run time, so anything from outside gets checked as it comes in.

Where the outside world gets checked
What arrivesWhat checks it
A request bodyZod schema in lib/contract.ts
A DoH responseZod schema in packages/dns/src/doh.ts
Domain Connect settingsZod schema, and HTTPS only
A domain someone typednormalizeDomain
A claim id in a URLparseClaimId
A database rowclaimFromRow and the to* helpers
Inside these edges, a value's type can be trusted. That's why each brand has exactly one cast, right after a validation.

Which JavaScript version we check against

The engine's is es2025, set in packages/tsconfig/strict.json. That's the newest numbered version TypeScript 7 accepts. Beyond it there's only esnext, which is a moving target rather than a stable version. The frozen experiments stay on es2023.

Why an editor can disagree with the build
WhereTypeScriptKnows es2025?Result
bun run typecheck7.0.2YesNo errors
TypeScript 66.0.3YesNo errors
Zed's default server, vtsls5.9.3NoErrors that aren't real
Tested by running each compiler over packages/core. An unknown target is rejected, and the compiler falls back to ES5, where even the u flag on a regex looks too new.

Testing that code doesn't compile

// @ts-expect-error verifiedAt is required
const bad: ClaimState = { status: "verified", lastSeenAt: at }

This is an assertion about the type system. If that line ever starts compiling, the directive itself becomes an error and bun run typecheck fails.

Predict it first
A claim's status came from the database, so the compiler doesn't know which one it is. Can you pass an illegal event to transition?