One Number Type for Everything
JavaScript has a single numeric type. There is no separate integer type: 7 and 7.0 are the same value, and typeof reports 'number' whether or not there is a decimal point. Every number is stored as a 64-bit double-precision float — the same format that C and Java call double. If you are coming from those languages, this is the biggest single difference to absorb.
That choice has two consequences you will meet in real work. The first is precision on large whole numbers. A double represents every integer exactly up to Number.MAX_SAFE_INTEGER, which is 9007199254740991. Above that, integers start losing precision, so 9007199254740992 + 1 evaluates to 9007199254740992. This is why databases and APIs often send very large ids as strings rather than numbers — sending them as numbers would corrupt them silently.
The second consequence is decimals. Values like 0.1 have no exact binary representation, in the same way that one third has no exact decimal representation. So 0.1 + 0.2 is 0.30000000000000004. That is not a JavaScript defect; the identical thing happens in Python, Java and C, and the last section of this lesson covers what to do about it.
There are also three special numeric values worth recognising on sight: Infinity from dividing by zero, -Infinity, and NaN from a calculation with no meaningful answer. None of the three throws an error. All three spread through any arithmetic that touches them, which is exactly what makes them worth spotting early rather than at the point where a nonsense figure reaches a user.
console.log(typeof 7); // 'number'
console.log(typeof 7.5); // 'number'
console.log(7 === 7.0); // true - the very same value
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740992 + 1); // 9007199254740992 - precision lost
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(5 / 0); // Infinity
console.log(-5 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(Infinity - Infinity); // NaN
console.log(typeof Infinity); // 'number' - None of
Infinity,-InfinityorNaNraises an error. They are ordinary values that flow onwards through your program, so the place they appear on screen is usually a long way from the place they were created.
Turning Text Into Numbers
Every value you read from a form field, a URL parameter or an HTML attribute is a string, so converting is a daily task rather than an occasional one. There are four ways to do it, and they behave differently on messy input — which is precisely when the difference matters.
Number(value) converts the whole string or fails. Number('42') is 42, Number('42px') is NaN, and Number('') is 0 — worth remembering, because it means an empty input box reads as zero rather than as missing, and a marks sheet then records a zero nobody entered. The unary plus, +value, does exactly the same job in one character; it is common in compact code and harder for a beginner to spot.
parseInt(value, 10) reads a whole number from the front of the string and stops at the first character it cannot use, so parseInt('42px', 10) is 42. That tolerance is useful for reading a CSS value like '16px' and dangerous for validating user input, because parseInt('42abc') quietly succeeds and hides the mistake. Always pass the second argument, the radix — passing 10 costs nothing and removes any question about how a leading zero or an 0x prefix will be read.
parseFloat is the same idea for decimals. The rule of thumb is short: use Number() when the entire string is supposed to be a number and anything else is an error, which is nearly always the case for form input; use parseInt or parseFloat only when you deliberately want to pull a number off the front of some longer text.
console.log(Number('42')); // 42
console.log(Number('42.5')); // 42.5
console.log(Number('42px')); // NaN
console.log(Number('')); // 0 <- an empty box reads as zero
console.log(Number(' 7 ')); // 7 - surrounding spaces are ignored
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number(true)); // 1
console.log(+'42'); // 42 - unary plus, same as Number()
console.log(parseInt('42px', 10)); // 42 - stops at the 'p'
console.log(parseInt('px42', 10)); // NaN - nothing usable at the front
console.log(parseFloat('3.14rem')); // 3.14
console.log(parseInt('3.99', 10)); // 3 - it truncates, it never rounds
// Reading a form field properly
function readMarks(text) {
const trimmed = text.trim();
if (trimmed === '') return null; // missing is not the same as zero
const n = Number(trimmed);
return Number.isNaN(n) ? null : n;
}
console.log(readMarks(' 82 ')); // 82
console.log(readMarks('82abc')); // null
console.log(readMarks('')); // null Number(' ')— a string of only whitespace — is also0. If a blank field and a zero must mean different things in your application, trim and check for an empty string before converting, exactly asreadMarksdoes above.
NaN: What It Means and How to Test For It
NaN stands for Not a Number, and it is the result of arithmetic that has no meaningful answer: Number('abc'), 0 / 0, Infinity - Infinity. Confusingly, typeof NaN is 'number' — which is actually correct, because it belongs to the number type. It is the number type's way of saying "this calculation failed".
NaN is the only value in JavaScript that is not equal to itself, so NaN === NaN is false and so is NaN == NaN. That means you cannot test for it with a comparison at all. Number.isNaN(value) is the correct test, and it is the only one you need.
There is an older global isNaN which is a different function and should be avoided. It converts its argument before testing, so isNaN('abc') returns true even though a string is not NaN, and isNaN('') returns false because an empty string converts to 0. Number.isNaN performs no conversion at all and answers the question you actually asked.
The reason NaN deserves this much attention is that it spreads. Any arithmetic involving NaN produces NaN, so a single unconverted form field at the top of a calculation turns the total, the average and the percentage into NaN — and because nothing throws, the page displays the word instead of failing visibly. When you see NaN on a screen, work backwards to the first value that came from outside your own code; that is where it started.
console.log(typeof NaN); // 'number'
console.log(NaN === NaN); // false - the only such value
console.log([NaN].includes(NaN)); // true - includes uses a different rule
console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN('abc')); // false - a string is not NaN
// The old global converts first, so it misleads in both directions
console.log(isNaN('abc')); // true <- misleading
console.log(isNaN('')); // false <- also misleading
// NaN spreads through everything downstream
const marks = [82, Number('abc'), 74];
const total = marks.reduce((sum, m) => sum + m, 0);
console.log(total); // NaN
console.log(`Total: ${total}`); // 'Total: NaN' - printed, not thrown
// Guard at the boundary instead
const clean = marks.filter(m => !Number.isNaN(m));
console.log(clean.reduce((sum, m) => sum + m, 0)); // 156 Array.prototype.includesfindsNaNbutindexOfdoes not, becauseindexOfuses strict equality andincludesuses a rule that treatsNaNas matching itself. It is the one place in the language where the two disagree.
Checking What Kind of Number You Have
Three checks cover nearly everything. Number.isInteger(value) is true only for whole numbers, and it is the right test for a quantity, a page number or an id. Number.isFinite(value) excludes both Infinity and NaN, which makes it the test to run before you display or store any computed number. Number.isSafeInteger(value) adds the range check, confirming the value is a whole number small enough to be represented exactly.
All three are the Number. versions, and none of them converts its argument. Number.isInteger('5') is false, because a string is not an integer no matter what it contains. The older global isFinite does convert, with the same misleading results as global isNaN, so prefer the Number. forms without exception.
A useful validation function combines them in a deliberate order: check it is a number at all, then that it is finite, then that it falls inside your acceptable range. Each check makes the next one meaningful, and doing them in the wrong order means the range comparison runs on a value that might be NaN — where every comparison is false, so the number quietly fails validation for the wrong reason.
Do not forget the plain typeof check at the front. In JavaScript a function can be handed anything at all, so a validator that assumes it received a number is a validator with a hole in it. Type first, then value.
console.log(Number.isInteger(7)); // true
console.log(Number.isInteger(7.5)); // false
console.log(Number.isInteger('7')); // false - no conversion happens
console.log(Number.isFinite(42)); // true
console.log(Number.isFinite(1 / 0)); // false
console.log(Number.isFinite(NaN)); // false
console.log(isFinite('42')); // true <- the global converts first
console.log(Number.isSafeInteger(9007199254740991)); // true
console.log(Number.isSafeInteger(9007199254740993)); // false
// Validation in the order that makes each step meaningful
function isValidMarks(value) {
if (typeof value !== 'number') return false;
if (!Number.isFinite(value)) return false;
return value >= 0 && value <= 100;
}
console.log(isValidMarks(82)); // true
console.log(isValidMarks(NaN)); // false
console.log(isValidMarks('82')); // false
console.log(isValidMarks(120)); // false - Every comparison involving
NaNisfalse, includingNaN >= 0andNaN <= 100. A range check therefore rejectsNaNby accident rather than on purpose, which is fine until someone reorders the conditions.
Formatting Numbers for Display
toFixed(n) returns a string with exactly n decimal places, padded with zeros where necessary. It is a display tool, and the fact that it returns a string is the single most common mistake made with it — adding to the result joins text instead of adding numbers, in exactly the way an unconverted form field does.
toString(radix) converts to another base: (255).toString(16) is 'ff' and (5).toString(2) is '101'. You will meet this with colour values and with bit flags, and parseInt(text, 16) converts back. Note the brackets around the numeric literal — writing 255.toString() is a syntax error, because the dot is read as the start of a decimal fraction.
toLocaleString and Intl.NumberFormat are what you actually want for money and for large numbers. They apply the grouping a locale expects — for 'en-IN' that means the lakh and crore grouping rather than grouping in threes — and they can attach a currency symbol and fix the number of decimals. All of it is built into the browser, with no library and no hand-written formatting code to maintain.
One caution about rounding for display: because the stored value is binary, a number that looks exactly halfway often is not, so toFixed can round in a direction that appears wrong. (1.005).toFixed(2) can return '1.00' for this reason. For money, the reliable answer remains keeping whole paise as integers and dividing only at the moment of display.
const value = 1234.5678;
console.log(value.toFixed(2)); // '1234.57' - a string
console.log(typeof value.toFixed(2)); // 'string'
console.log(value.toFixed(0)); // '1235'
console.log(Number(value.toFixed(2))); // 1234.57 - back to a number
// Other bases
console.log((255).toString(16)); // 'ff'
console.log((5).toString(2)); // '101'
console.log(parseInt('ff', 16)); // 255 - and back again
// Locale-aware grouping
console.log((1234567.5).toLocaleString('en-IN')); // Indian lakh grouping
console.log((1234567.5).toLocaleString('en-US')); // '1,234,567.5'
const rupees = new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
maximumFractionDigits: 0
});
console.log(rupees.format(1234567)); // a grouped rupee amount
const pct = new Intl.NumberFormat('en-IN', {
style: 'percent',
maximumFractionDigits: 1
});
console.log(pct.format(0.783)); // '78.3%' Intl.NumberFormatwithstyle: 'percent'expects a fraction, not a percentage. Pass0.783to get 78.3%; passing78.3gives you 7,830%.
Floating Point, and When You Need BigInt
Everything in this lesson comes back to one fact: numbers are binary floats. The practical rules that follow from it are short, and they are identical in every language that uses doubles, so learning them here saves you learning them again later.
Never compare two computed decimals with ===. Compare the size of their difference against a small tolerance — Math.abs(a - b) < Number.EPSILON — or, better, arrange not to be in that situation. Number.EPSILON exists for exactly this purpose and is the smallest meaningful tolerance for values near 1.
For money, keep integers. Store paise rather than rupees, keep quantities and unit prices as whole numbers, and divide once at the very end. Rounding then happens exactly where you decided it should, instead of accumulating invisibly across a hundred additions and leaving a total that is out by a paisa with no obvious cause.
BigInt is the escape hatch for whole numbers beyond the safe range. You write one with an n suffix — 9007199254740993n — and it holds arbitrarily large integers exactly. The catch is that you cannot mix a BigInt with an ordinary number in arithmetic: doing so throws a TypeError, and you must convert explicitly, which can lose precision going the other way. Use it only where you genuinely need it, which in most web work means very large identifiers.
// Never compare computed decimals directly
console.log(0.1 + 0.2 === 0.3); // false
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON); // true
// Money: keep integers, divide once at the end
const itemPaise = 19999; // i.e. 199.99 rupees
const qty = 3;
const totalPaise = itemPaise * qty;
console.log(totalPaise); // 59997
console.log((totalPaise / 100).toFixed(2)); // '599.97'
// BigInt for whole numbers beyond the safe range
const big = 9007199254740993n;
console.log(big + 1n); // 9007199254740994n
console.log(typeof big); // 'bigint'
// console.log(big + 1); // TypeError - cannot mix BigInt and Number
console.log(Number(big) + 1); // converts first, and loses precision Number(value)— convert the whole string or getNaN;Number('')is0parseInt(value, 10)— read a number off the front; always pass the radixNumber.isNaN(v)— the only correct NaN test;=== NaNnever worksNumber.isFinite(v)— excludesInfinityandNaNNumber.isInteger(v)— whole numbers only, with no conversiontoFixed(n)— a display string, not a numbertoLocaleString('en-IN')— correct grouping for Indian numbersNumber.MAX_SAFE_INTEGER— above this, whole numbers stop being exact
- If a total is out by a paisa, you are almost certainly adding decimals somewhere. Convert the whole calculation to whole units, confirm the figure is right, and only then divide for display.
