DEV Community

levent çelik
levent çelik

Posted on

The percentage helpers I wish someone had handed me on day one of JavaScript

Percentages are the math operation programmers re-invent the most. Every checkout flow has them, every dashboard has them, every pricing toggle has them, and yet I still see the same three off-by-one bugs in code reviews every month. Most of them come down to one thing: nobody wrote a small set of named helpers, so every component invents its own.

Here are the five percentage helpers I keep in a percent.ts file. None of them are clever. All of them have saved me at least one bug.

1. percent of value

The one everyone gets right:

export const percentOf = (value: number, percent: number): number =>
  (value * percent) / 100;

percentOf(250, 8.5); // 21.25
Enter fullscreen mode Exit fullscreen mode

But having it as a named function means your sales tax calculation reads as percentOf(price, taxRate) instead of (price * taxRate) / 100 repeated in 14 components. Less typing is the smaller win; the bigger one is that any bug you find lives in exactly one place.

The canonical use case is sales tax. The Sales Tax (VAT) Calculator on Equation Solver is the page I open when I cannot remember whether a quoted price is tax-inclusive or tax-exclusive (a surprisingly common ambiguity in invoices).

2. value plus a markup

The one almost everyone gets wrong by inlining the math:

export const addPercent = (value: number, percent: number): number =>
  value * (1 + percent / 100);

addPercent(100, 20); // 120
Enter fullscreen mode Exit fullscreen mode

Why a helper for this? Because price + price * 0.2 and price * 1.2 and price + percentOf(price, 20) all produce the same answer most of the time, and developers debate which is most readable in PRs forever. Pick one, name it, move on.

3. value minus a discount

Symmetric, but its own function so the call site is easier to grep:

export const subtractPercent = (value: number, percent: number): number =>
  value * (1 - percent / 100);

subtractPercent(100, 25); // 75
Enter fullscreen mode Exit fullscreen mode

Also: if you ever need to restore the original price from a discounted one, the inverse is discounted / (1 - percent / 100), which is one of those "obvious in retrospect" tricks that I have watched seniors fail at on a whiteboard. I keep that as a separate helper too.

4. percentage change between two values

My favorite, because it is what every dashboard "up X% from last month" claim secretly relies on:

export const percentChange = (oldValue: number, newValue: number): number => {
  if (oldValue === 0) return Infinity;
  return ((newValue - oldValue) / oldValue) * 100;
};

percentChange(80, 100); // 25
percentChange(100, 80); // -20
Enter fullscreen mode Exit fullscreen mode

Note the asymmetry: going from 80 to 100 is a 25% gain, but going from 100 to 80 is only a 20% loss. That asymmetry is the basis of half the misleading marketing claims you have ever read. A 50% discount followed by a 50% markup does not return to the original price; it leaves you 25% lower. Programmers who internalize this become impossible to mislead with charts.

For a clean visualization of this, the Inflation Calculator on Equation Solver shows what a percentage change compounded over many years actually does to purchasing power. It is the cleanest demo I know of why "3% inflation per year" is not a small number.

5. tip and tax in one go

Not a primitive, but the helper I actually open most often, mostly when traveling:

export function billTotal({
  subtotal,
  taxPercent = 0,
  tipPercent = 0,
  tipBasis = "preTax",
}: {
  subtotal: number;
  taxPercent?: number;
  tipPercent?: number;
  tipBasis?: "preTax" | "postTax";
}) {
  const tax = percentOf(subtotal, taxPercent);
  const tipBase = tipBasis === "preTax" ? subtotal : subtotal + tax;
  const tip = percentOf(tipBase, tipPercent);
  return { subtotal, tax, tip, total: subtotal + tax + tip };
}
Enter fullscreen mode Exit fullscreen mode

The tipBasis flag is the part most calculators forget to expose. In the US, tipping on the post-tax total is common but not universal; in many countries it is implicitly pre-tax or simply included. Naming the choice in the API forces the caller to think about it for half a second, which is exactly the right amount of friction.

A small style note

You will notice none of these helpers round. That is on purpose; rounding belongs at the display layer, not at the math layer. If percentOf(0.10, 8.5) is 0.0085, that is the right answer; it is up to your formatter to decide what shows up in the UI. Helpers that round eagerly have caused me more bugs than I can count, especially in cumulative sums where small errors compound.

If there is a sixth helper to add, it is probably a weightedAverage(values, weights) for portfolio-style aggregations. But percentages are the load-bearing five. Once they are named, the bugs go away, and PRs about "is this the right formula?" go from weekly to never.

Top comments (0)