How to Round to 2 Decimals in TypeScript (Without the 1.005 Bug)

The quickest way to round to 2 decimals in TypeScript is Math.round(value * 100) / 100, which returns a number. It breaks on halfway values like 1.005, though, so when every cent matters, shift the decimal point with exponent notation instead:

const rounded = Number(Math.round(Number(value + "e2")) + "e-2"); // 1.005 -> 1.01

I ran every snippet through TypeScript 7.0.2 and Node.js v22.22.2 in strict mode, so you’re seeing real console output.

Round to 2 decimals in TypeScript with Math.round()

Multiply by 100, round to the nearest integer, then divide by 100. Here it is on a cart subtotal and an 8.25% Texas sales tax:

function round2(value: number): number {
  return Math.round(value * 100) / 100;
}

const subtotal = 47.8567;                      // USD
console.log(round2(subtotal), round2(subtotal * 0.0825), round2(12));

Output:

47.86 3.95 12

You get a number back, and 12 stays 12 because numbers don’t store trailing zeros. More on typed helpers in functions in TypeScript.

Why does Math.round(1.005 * 100) / 100 return 1?

Most decimals can’t be stored exactly in binary. The literal 1.005 is a hair below 1.005, so 1.005 * 100 lands just under 100.5:

const prices: number[] = [1.005, 1.255, 2.675, 8.345];

for (const p of prices) {
  const naive = Math.round(p * 100) / 100;
  console.log(`${p} -> ${naive}   stored as ${p.toPrecision(21)}`);
}

Output:

1.005 -> 1   stored as 1.00499999999999989342
1.255 -> 1.25   stored as 1.25499999999999989342
2.675 -> 2.68   stored as 2.67499999999999982236
8.345 -> 8.35   stored as 8.34500000000000063949
Command Prompt output showing TypeScript Math.round rounding 1.005 to 1 because it is stored as 1.00499999999999989342
1.005 and 1.255 round down; 2.675 and 8.345 happen to round up.

2.675 is stored low too, but 2.675 * 100 rounds to exactly 267.5, so it’s luck either way. Same quirk as 0.1 + 0.2 when you sum an array in TypeScript.

Does Number.EPSILON fix rounding to 2 decimals?

A popular fix adds Number.EPSILON (about 2.2e-16) first. It rescues 1.005, but not every value:

const roundEps = (v: number) => Math.round((v + Number.EPSILON) * 100) / 100;

for (const p of [1.005, 1.255, 2.135, 4.015]) {
  console.log(p, "->", roundEps(p));
}
console.log(4.015 + Number.EPSILON === 4.015);

Output:

1.005 -> 1.01
1.255 -> 1.26
2.135 -> 2.13
4.015 -> 4.01
true

The nudge is sized for values near 1. Above that, it’s smaller than the gap between neighboring doubles, so 4.015 + Number.EPSILON is still 4.015. I don’t trust it for money.

Round to 2 decimals reliably with an exponent shift

A better trick moves the decimal point in the string. String(1.005) is "1.005", so "1.005e2" parses to exactly 100.5. My two helpers live in an .mts file, so Node.js runs it as an ES module:

// round.mts
export function shift(value: number, places: number): number {
  const [mantissa, exponent = "0"] = String(value).split("e");
  return Number(`${mantissa}e${Number(exponent) + places}`);
}

export function roundTo(value: number, decimals = 2): number {
  return shift(Math.round(shift(value, decimals)), -decimals);
}

Splitting on "e" handles values that already print in exponent form, like 1e-7:

import { roundTo } from "./round.mjs";

for (const v of [1.005, 2.135, 4.015, 1234.5678, 1e-7]) {
  console.log(String(v).padEnd(10), "->", roundTo(v));
}
console.log("one-liner on 1e-7:", Number(Math.round(Number(1e-7 + "e2")) + "e-2"));

Output:

1.005      -> 1.01
2.135      -> 2.14
4.015      -> 4.02
1234.5678  -> 1234.57
1e-7       -> 0
one-liner on 1e-7: NaN
Command Prompt output of a TypeScript roundTo helper rounding 1.005, 2.135 and 4.015 correctly with an exponent shift
Every tie rounds up, and 1e-7 works where the one-liner returns NaN.

Values from a form or an API are strings, so convert them first; here’s how to convert a string to a number in TypeScript.

Why toFixed(2) returns a string

toFixed(2) is for display. It returns a string and rounds the exact binary value, so 1.005 still gives "1.00":

const label = (19.5).toFixed(2);
console.log(label, typeof label, (1.005).toFixed(2), (2.675).toFixed(2));

Output:

19.50 string 1.00 2.67

Strict TypeScript catches the classic mistake of treating that string as a number:

const price: number = 24.999;
const rounded: number = price.toFixed(2);

Output:

round_tofixed_error.ts(2,7): error TS2322: Type 'string' is not assignable to type 'number'.
Command Prompt output of tsc error TS2322: Type string is not assignable to type number for TypeScript toFixed(2)
The compiler rejects the string from toFixed() as a number.

Round with roundTo() for math and call toFixed(2) only when you print. For padding and fixed widths, see formatting numbers to 2 digits in TypeScript.

How do you round negative numbers to 2 decimals?

Math.round() sends ties toward positive infinity, so -2.5 becomes -2 and -1.125 becomes -1.12, not -1.13:

import { roundTo } from "./round.mjs";

const roundHalfAway = (v: number) => Math.sign(v) * roundTo(Math.abs(v));

console.log(Math.round(-2.5), roundTo(1.125), roundTo(-1.125), roundHalfAway(-1.125), (-1.125).toFixed(2));

Output:

-2 1.13 -1.12 -1.13 -1.13

If you want refunds to mirror charges, round the absolute value and restore the sign, like roundHalfAway(). toFixed() already rounds this tie away from zero.

Round half to even (banker’s rounding) in TypeScript

Banker’s rounding sends ties to the even digit, so they don’t all drift up. It’s how rounding numbers in Python works by default:

import { roundTo, shift } from "./round.mjs";

function roundHalfEven(value: number, decimals = 2): number {
  const scaled = shift(value, decimals);
  const floor = Math.floor(scaled);
  const diff = scaled - floor;
  const up = diff > 0.5 || (diff === 0.5 && floor % 2 !== 0);
  return shift(up ? floor + 1 : floor, -decimals);
}

const ties = [0.125, 0.375, 0.625, 0.875];    // exact sum: 2
console.log("half up:  ", ties.map((t) => roundTo(t)));
console.log("half even:", ties.map((t) => roundHalfEven(t)));

Output:

half up:   [ 0.13, 0.38, 0.63, 0.88 ]
half even: [ 0.12, 0.38, 0.62, 0.88 ]

Those ties sum to exactly 2. Half-up results total 2.02; half-even results still total 2, which is why accounting code prefers it.

Round to 2 decimals with Intl.NumberFormat roundingMode

Since ES2023, Intl.NumberFormat takes a roundingMode option (Node.js 22 and current browsers). It returns a string, so use it for display:

const modes = ["halfExpand", "halfEven", "ceil", "floor", "trunc"] as const;
const values = [1.005, 19.991, -3.145];

for (const mode of modes) {
  const fmt = new Intl.NumberFormat("en-US", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
    roundingMode: mode,
  });
  console.log(mode.padEnd(10), values.map((v) => fmt.format(v).padStart(6)).join("  "));
}

Output:

halfExpand   1.01   19.99   -3.15
halfEven     1.00   19.99   -3.14
ceil         1.01   20.00   -3.14
floor        1.00   19.99   -3.15
trunc        1.00   19.99   -3.14
Command Prompt output comparing TypeScript Intl.NumberFormat roundingMode halfExpand, halfEven, ceil, floor and trunc at 2 decimals
Five rounding modes on three values; halfExpand is the default.

V8 rounds the shortest decimal form, so 1.005 becomes 1.01. The same API adds separators; see formatting numbers with commas in TypeScript.

Round up or down to 2 decimals (ceil, floor, trunc)

Swapping in Math.ceil or Math.floor looks easy, but multiplication errors bite harder:

import { shift } from "./round.mjs";

const ceil2 = (v: number) => shift(Math.ceil(shift(v, 2)), -2);
const floor2 = (v: number) => shift(Math.floor(shift(v, 2)), -2);
const trunc2 = (v: number) => shift(Math.trunc(shift(v, 2)), -2);

console.log("naive:", Math.ceil(1.1 * 100) / 100, Math.floor(1.13 * 100) / 100);
console.log("shift:", ceil2(1.1), floor2(1.13));
console.log("negative:", floor2(-1.234), trunc2(-1.234));

Output:

naive: 1.11 1.12
shift: 1.1 1.13
negative: -1.24 -1.23

1.1 * 100 is slightly above 110, so the naive ceiling jumps to 1.11. For negatives, floor moves away from zero; trunc moves toward it.

Rounding user input? Check that the string is a number first, then see converting a string to a double in TypeScript.

Which way to round to 2 decimals in TypeScript?

ApproachReturnsBest for
Math.round(x * 100) / 100numberQuick math
roundTo(x)numberExact half-up prices
toFixed(2)stringTwo-digit display
Intl.NumberFormatstringDisplay with a rounding mode

Next, you can try these number and array guides:

Frequently asked questions

How do I round a number to 2 decimal places in TypeScript?

Use Math.round(value * 100) / 100 for quick math, or an exponent-shift helper like roundTo() when values such as 1.005 must round up.

Why does 1.005 round to 1 instead of 1.01?

It’s stored as 1.00499999999999989342, so times 100 it’s just under 100.5 and Math.round() rounds down.

Does toFixed(2) return a number?

No, it returns a string. Number() converts it back but drops trailing zeros.

How do I