JavaScript gives you two precise predicates for integer checks. Number.isInteger(value)
answers “is this value a Number primitive with no fractional part?” Number.isSafeInteger(value)
goes further: the value must be an integer and within the IEEE-754 “safe” range where every integer is represented exactly. Neither function coerces non-numbers. If you pass a string, boolean, null
, or an object, they return false
. These definitions are standardized and consistent across modern engines.
Number.isInteger(3); // true
Number.isInteger(3.0); // true (no fractional part)
Number.isInteger(3.14); // false
Number.isInteger("3"); // false (no coercion)
Number.isSafeInteger(9007199254740991); // true
Number.isSafeInteger(9007199254740992); // false
Integers in a floating-point world
All ordinary JavaScript numbers are IEEE-754 double-precision floats, not a separate integer type. That’s why “integer-ness” is a property of a Number value, not a different runtime type. The engine deems a value an integer when it is finite and equal to its floor (or, equivalently, when the fractional part is zero). These semantics come directly from the language definition.
typeof 37; // "number"
Number.isInteger(37); // true
Number.isInteger(37.000); // true
Number.isInteger(-0); // true (yes, -0 is an integer)
What “safe” means and where the boundary sits
Because Numbers have 53 bits of precision in their significand, every integer from −(2^53 − 1) through +(2^53 − 1) is exactly representable—this is the safe integer range. Outside that closed interval, seemingly different integers can collapse to the same binary64 value. JavaScript exposes the boundaries as Number.MIN_SAFE_INTEGER
and Number.MAX_SAFE_INTEGER
. Number.isSafeInteger(x)
checks both integer-ness and that x
lies within these bounds.
Number.MAX_SAFE_INTEGER; // 9007199254740991 (2^53 - 1)
Number.MIN_SAFE_INTEGER; // -9007199254740991 (-(2^53 - 1))
Number.isInteger(9007199254740993); // true (mathematically an integer…)
Number.isSafeInteger(9007199254740993); // false (…but not safe as a Number)
9007199254740993 === 9007199254740992; // true (!) — precision lost
No coercion: objects, strings, and boxed numbers
Both methods require a Number primitive. They return false
for non-number types, including boxed numbers (new Number(5)
) and numeric strings. If you intend to validate user input, convert it first with Number()
, parseInt()
, or parseFloat()
, then check the resulting value with Number.isInteger
/Number.isSafeInteger
. The non-coercing behavior is part of the spec and intentionally differs from older global predicates.
Number.isInteger(new Number(5)); // false (object)
Number.isInteger("5"); // false (string)
const n = Number("5");
Number.isInteger(n); // true
Number.isSafeInteger(n); // true
Relationship to Number.isFinite
and Number.isNaN
For an integer check to succeed, the value must also be finite and not NaN
. You don’t need to test those separately—Number.isInteger
already implies them—but it’s useful to remember that Infinity
, -Infinity
, and NaN
fail the integer test even though they are of type number
. This follows from the same numeric abstract operations the spec uses for all Number predicates.
Number.isInteger(Infinity); // false
Number.isInteger(NaN); // false
Number.isSafeInteger(NaN); // false
BigInt vs Number: choose the right integer kind
If you need exact integers beyond the safe range, use BigInt
. The integer checkers here are for Number values only; they don’t accept BigInt
and will return false
for them. Use typeof x === "bigint"
(or logic specific to your domain) when you expect 64-bit+ magnitudes. The boundary constants and “safe” notion apply solely to Number, not BigInt.
const huge = 9007199254740993n; // BigInt
Number.isInteger(huge); // false (different type)
typeof huge === "bigint"; // true
Practical validation patterns you’ll actually use
For form inputs, API payloads, or CLI arguments, read once, then validate in two clear steps: convert to a Number, then assert integer-ness and optionally safety. This avoids accidental reliance on coercion and keeps your intent obvious in code review.
function readSafeInt(value) {
const n = Number(value);
return Number.isSafeInteger(n) ? n : null;
}
readSafeInt("42"); // 42
readSafeInt("42.0"); // 42
readSafeInt("42.1"); // null
readSafeInt("9007199254740993"); // null (unsafe)
If you’re normalizing data that may include decimals, clamp only after a precise test. Avoid Math.round
as a proxy for integer-ness; it can silently turn non-integers into integers. Let the predicate decide first.
function ensureInteger(n) {
if (!Number.isInteger(n)) throw new TypeError("Expected an integer");
return n;
}
ensureInteger(3.000); // ok
ensureInteger(3.14); // throws
Fast mental checks and boundary probes
When you’re unsure how a value behaves at the edges, probe it with arithmetic that would reveal precision loss. Values just outside the safe range will behave strangely under equality and +/−1 increments—another signal to switch to BigInt or redesign storage. The examples below illustrate this cliff.
const a = 9007199254740991; // MAX_SAFE_INTEGER
const b = a + 1; // 9007199254740992
const c = a + 2; // 9007199254740992 (!) same as b
Number.isInteger(b); // true
Number.isSafeInteger(b); // false
b === c; // true — distinct integers collapsed
References
Source | What it covers |
---|---|
MDN: Number.isInteger() | Definition, non-coercing behavior, examples, and spec links. |
MDN: Number.isSafeInteger() | “Safe” definition, behavior, and examples. |
MDN: Number.MAX_SAFE_INTEGER | Boundary constant and explanation of 2^53 − 1. |
MDN: Number.MIN_SAFE_INTEGER | Negative boundary constant −(2^53 − 1). |
MDN: Number | Numbers are IEEE-754 doubles; BigInt exists but is a different type. |
ECMAScript® Language Specification | Normative algorithms for Number.isInteger and Number.isSafeInteger . |