Before you begin
Why this matters
Numbers in your programs often start as text: a typed value, a line in a file, or data from a server. Some of them are too long for a number, and some need bigint arithmetic. If you know how the two types differ, you can predict a changed id, a bigint division, and a comparison that is false, before they reach your output.
What this helps you reason about
- Predict when turning text into a number changes its digits, and use BigInt instead.
- Predict /, ===, ==, and sort with bigints, and convert between bigint and number on purpose.
- Say why Number.EPSILON is a useful tolerance only for numbers near 1.
Java has int, long, and double. Python has int and float. JavaScript, which TypeScript runs as, has one number type for all of them, and a second type, bigint, for whole numbers of any size. Integers, signs, and overflow showed where a number stops holding every whole number. This lesson shows why, what happens to long numbers read from text, and where number and bigint do not fit together.
Already know this? The short version
- There is one number type, an IEEE 754 64-bit double. The gap between stored numbers doubles with each power of two, so Number.EPSILON (2^-52) is the gap at 1 only, and whole numbers are exact up to Number.MAX_SAFE_INTEGER.
- Number(text), parseInt, and JSON.parse round a long id without an error; BigInt(text) keeps every digit and throws a SyntaxError for anything but a whole number.
- bigint arithmetic is exact, / cuts toward zero, and mixing with number is a compile error, or a TypeError while the program runs when one side has type any. Math functions refuse bigints; bitwise operators on bigints keep every bit.
- 42 === 42n is false, 42 == 42n is true, and < works across the two types. The section “What goes wrong with numbers and bigints in real programs?” covers ids from servers, JSON.stringify, money, comparisons, and sorting.
Words in this lesson (8)
- floating-point number
- A number stored in binary, with about 16 significant decimal digits. Most decimal fractions, such as 0.1, are stored as the nearest binary value, not exactly.
- Number.EPSILON
- The gap between 1 and the next number that a TypeScript number can store, about 2.2 × 10^-16. Between larger numbers the gap is larger.
- safe integer
- A whole number from -(2^53 - 1) to 2^53 - 1. A TypeScript number holds each of them exactly; beyond them, some whole numbers cannot be stored.
- parse
- To read text and build a value from it, such as the number 21 from the text "21". Parsing fails when the text does not have the expected form.
- JSON
- A text format for data, such as {"sample": 42}, that many servers send and receive. JSON.parse reads JSON text into values, and JSON.stringify writes values as JSON text.
- bigint
- TypeScript’s type for whole numbers of any size, written with n after the digits, such as 10n. It never overflows, and it cannot be mixed with number in arithmetic.
- any
- The type that turns type checking off: a value of type any may be used as every type, and the compiler checks nothing about it.
- TypeError
- The error JavaScript throws when an operation gets a value it cannot work with, such as reading a property of undefined.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / One type
What does one number type for everything mean?
Integers, signs, and overflow showed that a number holds every whole number exactly up to 2^53 - 1, which is Number.MAX_SAFE_INTEGER. Floating-point numbers showed that 0.1 is stored as a nearby binary value. Both facts come from one design. JavaScript has a single number type, a 64-bit floating-point number in the IEEE 754 format, often called a double. typeof x gives the name of the type of x as text: typeof 3 and typeof 3.5 both give "number", and 3 === 3.0 is true.
Picture the stored numbers as points on a line. Between two neighbouring points there is a gap, and a result that lands in a gap is rounded to the nearest point. The gaps grow with the numbers: each time a number doubles, the gap doubles too. Number.EPSILON is the gap just above 1, about 2.2 × 10^-16. Near 1000 the gap is 512 times larger. At 2^53 the gap reaches 2, so from there on only every second whole number can be stored. That is where the safe integers end.
So Number.EPSILON is a useful tolerance only for numbers near 1. Math.abs(x) gives x without its sign. The test Math.abs(a - b) < Number.EPSILON says that 1000.1 + 0.2 and 1000.3 are different, because the rounding error near 1000 is larger than Number.EPSILON.
console.log(typeof 3, typeof 3.5, 3 === 3.0); // number number true
console.log(1 + Number.EPSILON); // 1.0000000000000002
console.log(1000 + Number.EPSILON === 1000); // true
// 2 ** 53 is 2 to the power 53
console.log(2 ** 53 + 1 === 2 ** 53); // true Every number is one 64-bit floating-point type. The gap between stored numbers grows with their size, so Number.EPSILON fits only near 1, and whole numbers stop being exact past 2^53 - 1.
Coming from Java: A Java long holds every 64-bit id exactly. A TypeScript number has 53 binary digits, so the same id can lose its last digits without an error.
Precisely: where the gaps come from
An IEEE 754 binary64 value has 1 sign bit, 11 exponent bits, and 52 fraction bits. With the leading 1 that is not stored, that gives 53 binary digits. For x from 2^k up to 2^(k+1), the gap is 2^(k - 52); Number.EPSILON is 2^-52. A tolerance that grows with the numbers is Number.EPSILON * Math.max(Math.abs(a), Math.abs(b)), but the right tolerance depends on how many operations produced the values.
Experiment
Add Number.EPSILON to 1 and to 1000
Number.EPSILON is the gap between 1 and the next stored number, so 1 + Number.EPSILON is a new number. Near 1000 the gap is 512 times larger, so 1000 + Number.EPSILON lands between two stored numbers and is rounded back to 1000. Already at 2 the gap is twice Number.EPSILON: 2 + Number.EPSILON lies exactly halfway, and a result exactly halfway goes to the neighbour whose last binary digit is 0, which is 2. Past Number.MAX_SAFE_INTEGER the gap is 2, so limit + 1 and limit + 2 are both stored as 2^53.
Why it matters Use Number.EPSILON as a tolerance only for numbers near 1. Past 2^53 - 1, two different whole numbers can become one number.
Predict both lines. Then change 1000 to 2 on line 1 and predict whether next === start is still true.
const start = 1000;
const next = start + Number.EPSILON;
console.log(1 + Number.EPSILON === 1, next === start);
const limit = Number.MAX_SAFE_INTEGER;
console.log(limit + 1 === limit + 2);
What to look for
At the first stop, on line 1, every name shows not initialized yet, because no line has run. Step past line 2: start shows 1000, and next shows 1000 as well. The run prints false true, then true. After the edit, start and next both show 2, and the run prints the same two lines. The stepper sometimes stops on the same line twice; that is normal for this debugger.
Expected output
false true
true02 / From text
What happens to a long whole number read from text?
Numbers often arrive as text: typed into a form, read from a file, or sent by a server. Printing values and reading input showed two ways to parse text into a number. Number(text) reads the whole text and gives NaN when any part of it is not a number. parseInt(text) reads the digits at the start, so parseInt("12 kg") is 12. Both build a number, so both round a long whole number: Number("9007199254740993") is 9007199254740992, and nothing reports the change.
JSON is a text format for data, such as {"sample": 42}, that many servers send and receive. JSON.parse(text) reads JSON text and builds the values it describes. Every number in the text becomes a number, and it is rounded in the same way.
BigInt(text) parses the text into a bigint and keeps every digit. It is strict: it accepts only a whole number. BigInt("12 kg") and BigInt("1.5") throw a SyntaxError, the error for text that does not have the expected form: Cannot convert 12 kg to a BigInt. To keep a long id exact, never let it become a number. Keep it as text, or parse it with BigInt. Once a number has lost digits, BigInt(number) cannot bring them back, because it converts the rounded value.
const digits = "9007199254740993";
console.log(Number(digits)); // 9007199254740992
console.log(parseInt(digits)); // 9007199254740992
console.log(BigInt(digits)); // 9007199254740993n
const lab = JSON.parse('{"sample": 9007199254740993}');
console.log(lab.sample); // 9007199254740992 Number, parseInt, and JSON.parse round a long whole number without an error; BigInt(text) keeps every digit.
Coming from Python: Python’s json.loads reads 9007199254740993 as an exact int. JSON.parse reads it as the nearest number, 9007199254740992.
Precisely: what each one accepts
Number("1e3") is 1000, while parseInt("1e3") is 1. Number, parseInt, and BigInt all read "0x1f" as 31. Like Number, BigInt gives 0n for an empty or blank string. The JSON format itself puts no limit on numbers, but RFC 8259 warns that numbers beyond the safe integers may not be exchanged exactly between programs.
Experiment
Parse a 16-digit sample number two ways
Number builds a floating-point number. 9007199254740993 lies past 2^53, where only even whole numbers are stored, so it becomes the stored number next to it. BigInt builds a bigint and keeps every digit. For text that is not a whole number, Number gives NaN and the program continues, while BigInt throws.
Why it matters Parse a long id with BigInt, or keep it as text. Number and parseInt can change its digits without any error.
Predict both lines. Then change the text on line 1 to "12 kg" and predict what each conversion does.
const digits = "9007199254740993";
const asNumber = Number(digits);
const asBig = BigInt(digits);
console.log(asNumber, asBig);
console.log(String(asNumber) === digits);
What to look for
Step past line 2: asNumber shows 9007199254740992, one less than the digits in the text. Step past line 3: asBig shows 9007199254740993n, with every digit kept. The run prints 9007199254740992 9007199254740993n, then false. After the edit, asNumber shows NaN at line 3, and the run stops at line 3 with SyntaxError: Cannot convert 12 kg to a BigInt.
Expected output
9007199254740992 9007199254740993n
false03 / bigint
What can a bigint do, and what can it not mix with?
A bigint is a whole number of any size, written with n after the digits, such as 7n. typeof 7n gives "bigint". Arithmetic between two bigints is always exact. Division is the one surprise: a bigint has no fraction, so / cuts the fraction off, toward zero. 7n / 2n is 3n, and -7n / 2n is -3n, like Math.trunc(-7 / 2). Math.floor(-7 / 2) rounds down instead, to -4. % gives what is left over: 7n % 2n is 1n.
TypeScript refuses to mix the two types in arithmetic. With tubes a bigint and racks a number, tubes / racks gives the compile error Operator '/' cannot be applied to types 'bigint' and 'number'. Convert one side yourself. BigInt(x) turns a whole number into a bigint, and throws a RangeError for 2.5, which is not whole. Number(big) turns a bigint into the nearest number.
The compiler can check only values whose types it knows. JSON.parse returns a value of type any, the type that turns type checking off. When a bigint meets a number that came from such a value, the program compiles. The operation then throws a TypeError, the error JavaScript throws when an operation gets a value it cannot work with: Cannot mix BigInt and other types, use explicit conversions.
Math functions, such as Math.max and Math.round, take numbers only, and throw a TypeError for a bigint. Bitwise operators work on both types. On numbers they work on 32 bits, as Masks, shifts, and bitwise operations showed; on bigints they keep every bit. (2 ** 40) | 1 is 1, but (2n ** 40n) | 1n is 1099511627777n.
const tubes = 7n;
console.log(typeof tubes); // bigint
console.log(tubes / 2n, tubes % 2n); // 3n 1n
console.log(-tubes / 2n); // -3n
console.log(Math.floor(-7 / 2)); // -4
console.log(tubes + BigInt(3)); // 10n A bigint is exact and has no fraction: / cuts toward zero, and every mix with a number needs BigInt or Number written in the code.
Coming from Go: Go refuses int64 + float64 until one side is converted. TypeScript does the same for bigint and number, but only while the compiler knows both types.
Precisely: converting large values
BigInt(x) converts the number that is stored, so BigInt(2 ** 53 + 1) is 9007199254740992n. Number(big) rounds to the nearest number: Number(2n ** 64n) is 18446744073709552000. Unary +, as in +7n, throws a TypeError, so convert with Number(7n) instead.
Experiment
Divide bigints, then mix in a parsed number
A bigint has no fraction, so 7n / 2n cuts 3.5 to 3n, and -7n / 2n cuts -3.5 toward zero, to -3n. Math.floor rounds down instead, to -4. order has type any, so TypeScript checks nothing about order.racks. The mix of a bigint and a number is found only when line 4 runs.
Why it matters Convert a value from JSON.parse with BigInt before bigint arithmetic, and use bigint division only where cutting toward zero is what you want.
Predict both lines. Then replace BigInt(order.racks) on line 3 with order.racks, and predict whether the program compiles and what happens.
const order = JSON.parse('{"racks": 2}');
const tubes = 7n;
const racks = BigInt(order.racks);
console.log(tubes / racks, tubes % racks);
console.log(-tubes / racks, Math.floor(-7 / 2));
What to look for
Step past line 1: order shows {racks: 2}, with a plain number. Step past line 3: racks shows 2n. The run prints 3n 1n, then -3n -4. After the edit, the program compiles. At line 4, racks shows 2, with no n, and the run stops at line 4 with TypeError: Cannot mix BigInt and other types, use explicit conversions.
Expected output
3n 1n
-3n -404 / Comparing
How do a bigint and a number compare?
Equality and comparison showed that === compares two values without converting them. A number and a bigint always have different types, so 42 === 42n is false. == converts before it compares, so 42 == 42n is true. < and > compare the values across the two types: 42 < 50n is true.
When TypeScript knows both types, it refuses === and == between them: This comparison appears to be unintentional because the types 'number' and 'bigint' have no overlap. It still allows < and >.
When one side has type any, the compiler reports nothing, and === is quietly false. Do not rely on == to hide the difference. Convert one side, so both have the same type, and then compare with ===.
const scan = JSON.parse('{"sample": 42}');
const stored = 42n;
console.log(scan.sample === stored); // false
console.log(scan.sample == stored); // true
console.log(BigInt(scan.sample) === stored); // true A number and a bigint are never ===. Convert one side, so both have the same type, before comparing.
Coming from Java: Long.valueOf(42).equals(42) is false in Java, because 42 becomes an Integer. 42 === 42n is false here for the same kind of reason: the two values have different types.
Experiment
Compare a parsed number with a stored bigint
=== is false for two values of different types, and a number and a bigint always have different types. == converts before it compares, and < compares the values. With JSON.parse, scan has type any, so TypeScript checks nothing. With an object written in the code, TypeScript knows that scan.sample is a number and refuses both comparisons. It still allows <.
Why it matters When one side comes from JSON.parse, convert it to the other side’s type first, then compare with ===.
Predict both lines. Then replace JSON.parse('{"sample": 42}') on line 1 with { sample: 42 } and predict whether the program compiles.
const scan = JSON.parse('{"sample": 42}');
const stored = 42n;
console.log(scan.sample === stored, scan.sample == stored);
console.log(scan.sample < 50n, BigInt(scan.sample) === stored);
What to look for
Step past line 2: scan shows {sample: 42}, and stored shows 42n. The run prints false true, then true true. After the edit, nothing runs: the compiler reports This comparison appears to be unintentional because the types 'number' and 'bigint' have no overlap. for line 3, twice: once for === and once for ==.
Expected output
false true
true true05 / In production
What goes wrong with numbers and bigints in real programs?
Each of these bugs gives a wrong value with no error, or an error far from its cause.
Large ids from a server. Many databases use 64-bit ids, which can pass 2^53. JSON.parse reads 1793528427040600065 as 1793528427040600000, and the program shows or changes a different record. Ask for ids as JSON strings, and keep them as strings or parse them with BigInt. Sending a bigint back fails too: JSON.stringify throws TypeError: Do not know how to serialize a BigInt. Write String(id) into the object instead.
Money in floating-point numbers. A price typed as 19.99 and multiplied by 100 is 1998.9999999999998 cents. Convert to whole cents once, with Math.round, as Floating-point numbers showed, and add whole cents after that.
A number compared with a bigint. One part of a program keeps ids as bigints, and another gets them from JSON.parse as numbers. The parsed value has type any, so === compiles and is always false, and a search finds nothing. Convert one side, so both have the same type.
Sorting bigints. samples.sort() puts the elements of the array samples in order, and with no argument it compares them as text: [10n, 9n, 100n] becomes [10n, 100n, 9n]. sort can take a compare function. It gets two elements and returns a negative number when the first goes first, a positive number when the second goes first, and 0 otherwise. The usual (a, b) => a - b does not compile for bigints, because a - b is a bigint, not a number. Compare with < and > instead. Below, a < b ? -1 : … is the conditional operator: it gives -1 when a < b is true, and otherwise the value after the colon.
const samples = [10n, 9n, 100n];
samples.sort();
console.log(samples); // [ 10n, 100n, 9n ]
samples.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
console.log(samples); // [ 9n, 10n, 100n ] Keep long ids as text or bigints from the moment they arrive, money in whole cents, and both sides of a comparison in one type.
Precisely: other ways out
JSON.stringify takes a replacer function, which can turn every bigint into a string as it writes. The compile error for a - b in sort is Argument of type '(a: bigint, b: bigint) => bigint' is not assignable to parameter of type '(a: bigint, b: bigint) => number'.
Experiment
Sort an array of bigints
sort() with no argument turns each element into text and compares the texts character by character: "100" comes before "9", because "1" comes before "9". With a compare function, sort asks the function about two elements at a time. a < b ? -1 : … is the conditional operator: it gives -1 when a < b is true, and otherwise the value after the colon. The function returns a number, as sort requires.
Why it matters Give sort a compare function that returns a number, built with < and >, never a - b.
Predict the printed array. Then replace line 2 with samples.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); and predict again.
const samples = [10n, 9n, 100n];
samples.sort();
console.log(samples);
What to look for
Step past line 2: samples changes from [10n, 9n, 100n] to [10n, 100n, 9n], and the run prints [ 10n, 100n, 9n ]. After the edit, the stepper enters a frame named arrow function (line 2) three times, with a and b holding two of the bigints each time. Back at line 3, samples shows [9n, 10n, 100n], and the run prints [ 9n, 10n, 100n ].
Expected output
[ 10n, 100n, 9n ]06 / Try it
Can you predict what a long number becomes?
Follow the text into a number and into a bigint. Decide whether the number is safe, whether it is exact, and what the bigint division keeps.
const text = "9007199254740998";
const asNumber = Number(text);
const asBig = BigInt(text);
console.log(Number.isSafeInteger(asNumber), BigInt(asNumber) === asBig);
console.log(asBig / 4n, asBig % 4n, typeof asBig);
What are the two output lines?
Reveal the reasoning
false true, then 2251799813685249n 2n bigint. 9007199254740998 is larger than 2^53 - 1, so it is not a safe integer. It is even, though, and past 2^53 even whole numbers are still stored, so Number keeps it exactly, and BigInt(asNumber) equals asBig. Not safe means that a number may have been changed, not that it was. 9007199254740998 / 4 is 2251799813685249.5, and bigint division cuts the .5 off. The remainder is 9007199254740998 - 2251799813685249 × 4 = 2.
Practice tickets
- TS-201 · The next claim number repeats one already used → Free
- TS-202 · Scans that end late in a second show one second too few → Sign in · Pro
- TS-203 · Every wristband scan shows unknown patient → Sign in · Pro
Keep exploring
Primary references
- ECMAScript specification: The Number Type ↗
- ECMAScript specification: The BigInt Type ↗
- MDN: Number.EPSILON ↗
- MDN: Number.isSafeInteger ↗
- MDN: BigInt ↗
- MDN: JSON.parse ↗
- MDN: JSON.stringify (BigInt values) ↗
- MDN: Array.prototype.sort ↗
- RFC 8259: The JSON Data Interchange Format, section 6 (Numbers) ↗
- TypeScript 3.2 release notes: BigInt ↗