Parse a validated decimal string directly with BigInt, then use n % 2n !== 0n. Never route large integers through Number.
Use BigInt when an exact integer can exceed JavaScript's safe Number range. For a BigInt value, the oddness check is n % 2n !== 0n. Both constants have the n suffix so the operation stays within the BigInt type.
Parse and check exact BigInt values
function isOddBigInt(n) {
if (typeof n !== 'bigint') {
throw new TypeError('Expected a bigint');
}
return n % 2n !== 0n;
}
function parseDecimalInteger(text) {
if (typeof text !== 'string' || text.trim() !== text ||
!/^[+-]?[0-9]+$/.test(text)) {
throw new TypeError('Expected signed decimal digits');
}
return BigInt(text);
}
const value = parseDecimalInteger('9007199254740993');
console.log(isOddBigInt(value)); // true
console.log(isOddBigInt(-value)); // true
console.log(isOddBigInt(0n)); // false
Define the decimal input grammar
The parser defines a narrow decimal grammar: one optional leading sign followed by one or more ASCII digits. It permits leading zeroes, but rejects an empty string, surrounding whitespace, decimal points, exponent notation, separators, and hexadecimal prefixes. The explicit whitespace check makes the parser’s no-whitespace policy explicit.
That policy is stricter than simply calling BigInt on arbitrary text. Keep it visible so a caller knows exactly what the input field accepts. If your application intentionally accepts surrounding spaces, trim the value before this parser and document that boundary behavior.
Preserve precision and numeric types
Most importantly, the parser passes the original string directly to BigInt. Do not insert Number(text) or parseInt(text) in between. An earlier conversion can round away a significant final digit before BigInt receives the value. The BigInt reference explains the separate numeric type and conversion concerns.
Negative odd BigInts can have a remainder of -1n when divided by 2n. Checking for a nonzero remainder handles either sign. Mixing 2 with a BigInt operand throws instead of automatically upgrading the Number. The remainder reference documents both behaviors.
Test parsing and serialize exact values
Exercise the text boundary independently from the calculation:
for (const text of ['', '3.5', '1e3', '0xff', '7\n']) {
try {
parseDecimalInteger(text);
throw new Error('Unexpected acceptance');
} catch (error) {
if (!(error instanceof TypeError)) throw error;
}
}
When sending the value through JSON, serialize the decimal text, for example { id: value.toString() }. Parse that text explicitly on receipt. Standard JSON numeric values do not provide a BigInt type, and the default JSON serializer does not directly serialize BigInt values.
Use this approach for large counters or exact numeric identifiers whose digits matter. If an identifier is not conceptually a number, preserve it as text throughout instead; formatting details such as leading zeroes may be meaningful even though they do not affect parity.
The function intentionally rejects even small Number values such as 3. If your code has a mixed numeric interface, choose and document a conversion policy before invoking it. Keeping that policy outside the predicate makes accidental Number input detectable during testing.