DEV Community

Cover image for The third enum variant that stopped my tax calculator from lying
Mark
Mark

Posted on

The third enum variant that stopped my tax calculator from lying

I built a small thing that estimates what a US pay raise actually lands in your pocket after federal tax, FICA, state tax, and inflation. The arithmetic is the boring part. The part that took three rewrites was deciding how to represent fifty states plus DC when you don't actually have clean data for all of them on day one.

This is a writeup of that one decision, because it generalizes well beyond taxes.

The problem isn't the math, it's the gaps

Federal brackets are easy. There's one set of numbers, the IRS publishes them, and progressive tax is a five-line loop:

function taxFromBrackets(taxable: number, brackets: Bracket[]): number {
  if (taxable <= 0) return 0;
  let tax = 0;
  let prevCap = 0;
  for (const { upTo, rate } of brackets) {
    const band = Math.min(taxable, upTo) - prevCap;
    if (band <= 0) break;
    tax += band * rate;
    prevCap = upTo;
  }
  return tax;
}
Enter fullscreen mode Exit fullscreen mode

State tax is the same loop with different numbers. The trouble is that "different numbers" hides a lot. Nine states don't tax wage income at all. A couple (New Hampshire, Tennessee) don't tax wages but have historically taxed other things, so you can't just lump them with Texas without a footnote. Some states publish 2026 figures early; others inflation-index their brackets and won't publish the new thresholds until mid-year. And when I started, I had verified numbers for about ten states and unverified-but-plausible numbers for the rest.

That last category is the dangerous one. The easy move is to ship the plausible numbers and fix them later. The problem is that a wrong tax estimate doesn't look wrong. It looks like a number. Someone in Oregon types in their salary, sees a confident dollar figure, and has no way to know it was a placeholder I never got around to checking.

Two variants felt like enough. It wasn't.

My first model was the obvious one:

type StateTax =
  | {