Numbers
number, bigint and typed arrays in TypeScript 7.0 (also checked with 6.0) under strict, with
ES2025 built-ins: literals, division and remainder, the 2⁵³ limit and BigInt, 32-bit bit
operators, float traps, Math and number theory, modular arithmetic, parsing and formatting, typed
arrays, costs, and the integer idioms DS&A solutions are built from. Every example runs on Node 24
LTS, Node 26 and Bun 1.4. The Python side is in Python numbers, mapped
in Coming from Python.
Types & literals
| Type | Literals | Notes |
|---|---|---|
number | 42, -7, 3.14, 1e9, 1_000_000, 0xff, 0o17, 0b1011 | one IEEE 754 double for integers and fractions alike |
bigint | 42n, 2n ** 64n, BigInt("9007199254740993") | exact, unbounded integers; never mixes with number |
| special values | Infinity, -Infinity, NaN, -0 | all of type number |
| literal types | 0 | 1, -1, 10n | a type that holds exactly those values |
Number, BigInt | wrapper objects and conversion functions | annotate with number and bigint, never Number |
| typed arrays | new Int32Array(n), new Float64Array(n) | fixed-size arrays of raw machine numbers (Typed arrays) |
[typeof 42, typeof 4.2, typeof 42n];
// ["number", "number", "bigint"]
1e9 === 1_000_000_000; // true: 1e9 is an integer here
Number.isInteger(5.0); // true: 5.0 and 5 are one value
0.1 + 0.2; // 0.30000000000000004
2n ** 64n; // 18446744073709551616n
Number.MAX_SAFE_INTEGER; // 9007199254740991There is no integer type. An integer is a number whose fraction is zero, and every integer up to
2⁵³ is stored exactly, so 1e9 is the integer 1 000 000 000 (in Python it would be a float). V8
keeps small integers in the value word itself ("Smis") instead of boxing them as heap doubles; that
is an implementation detail that changes speed, never a result.
Arithmetic & division
| Expression | Result | Rule |
|---|---|---|
7 / 2, 6 / 3 | 3.5, 2 | / is real division; there is no integer-division operator |
Math.trunc(-7 / 2) | -3 | toward 0, like / on ints in C and Java |
Math.floor(-7 / 2) | -4 | toward −∞, like Python's // |
-7 % 2, 7 % -2 | -1, 1 | % is a remainder: it takes the dividend's sign |
((-7 % 2) + 2) % 2 | 1 | floored mod, always in [0, m) for m > 0 |
2 ** 10, 2 ** -1 | 1024, 0.5 | |
-2 ** 2 | SyntaxError | write (-2) ** 2 or -(2 ** 2); TypeScript reports TS17006 |
2 ** 3 ** 2 | 512 | ** is right-associative: 2 ** 9 |
Math.round(2.5), Math.round(-2.5) | 3, -2 | ties go toward +∞: not to even, not away from 0 |
Math.round(-0.4) | -0 | prints as 0; Object.is tells them apart |
(2.5).toFixed(0), (-2.5).toFixed(0) | "3", "-3" | a string; ties go away from 0, judged on the stored value |
(1.005).toFixed(2) | "1.00" | 1.005 is stored as 1.00499999999999989… |
Math.abs(-5), Math.sign(-3) | 5, -1 | |
Math.max(3, 7), Math.max() | 7, -Infinity | Math.max(1, NaN) is NaN |
const a = -7;
const b = 2;
[Math.trunc(a / b), Math.floor(a / b)]; // [-3, -4]
[a % b, ((a % b) + b) % b]; // [-1, 1]
// floored mod: the result takes the divisor's sign
const mod = (x: number, m: number): number =>
((x % m) + m) % m;
mod(-1, 5); // 4: index -1 wraps to the last slot
mod(12, 5); // 2
// ceiling division of non-negative integers
const ceilDiv = (x: number, y: number): number =>
Math.floor((x + y - 1) / y);
ceilDiv(7, 2); // 41/6
-7 / 2 is -3.5, between -4 and -3. Integer division has to pick one of them.
For safe integers a and b, Math.floor(a / b) and Math.trunc(a / b) are the exact integer
quotient: the distance from a / b to the nearest integer is at least 1 / |b|, more than the
rounding error of the division. Past 2⁵³ use BigInt, whose / truncates.
Safe integers & BigInt
| Fact | Value or rule |
|---|---|
Number.MAX_SAFE_INTEGER | 2 ** 53 - 1 = 9007199254740991, the last n with n and n + 1 both exact |
Number.isSafeInteger(x) | an integer within ±(2⁵³ − 1) |
| past 2⁵³ | doubles skip integers: 2 ** 53 + 1 === 2 ** 53 |
a * b with a, b near 10⁹ | about 10¹⁸, far past 2⁵³: silently rounded |
10n, BigInt(10), BigInt("123") | exact; BigInt(1.5) throws RangeError |
1n + 1 | TypeError at run time, TS2365 at compile time: convert one side |
-7n / 2n, -7n % 2n | -3n, -1n: truncates like Math.trunc |
Number(big) | the nearest double: Number(2n ** 64n) is 18446744073709552000 |
BigInt.asIntN(32, x), BigInt.asUintN(64, x) | wrap to a signed or unsigned width |
| comparisons | 1n < 2 and 1n == 1 work; 1n === 1 is false (different types) |
Math.*, JSON.stringify, >>> | reject bigint with a TypeError |
const P = 2 ** 53;
P + 1 === P; // true
Number.isSafeInteger(P - 1); // true
Number.isSafeInteger(P); // false
const MOD = 1_000_000_007;
const x = 999_999_999;
(x * x) % MOD; // 63: x * x was rounded
Number((BigInt(x) * BigInt(x)) % BigInt(MOD)); // 64
BigInt.asIntN(32, 2n ** 31n); // -2147483648n
BigInt.asUintN(32, -1n); // 4294967295n
-7n / 2n; // -3n1/6
Below 2**53 every integer is a double, one apart. Number.MAX_SAFE_INTEGER is 2**53 - 1 = 9007199254740991.
const n = 10n;
try {
// @ts-expect-error: TS2365, bigint + number
n + 1;
} catch (e) {
console.log((e as Error).name); // "TypeError"
}
n + 1n; // 11n
Number(n) + 1; // 11BigInt arithmetic allocates a new value per operation. For modular products of numbers below
10⁹ that costs less than it sounds: 5 million BigInt products took about 1.5× as long as the
split mulmod in Modular arithmetic on Node 24. Pick BigInt for
correctness first.
Bits
| Operator | Meaning | TypeScript detail |
|---|---|---|
a & b, a | b, a ^ b | and, or, xor | both sides go through ToInt32 first: the result is a signed 32-bit integer |
~x | not | -x - 1 within 32 bits: ~5 === -6 |
x << k | shift left | the count is taken mod 32: 1 << 31 === -2147483648, 1 << 32 === 1 |
x >> k | arithmetic shift right | keeps the sign: -7 >> 1 === -4 |
x >>> k | logical shift right | unsigned 32-bit result: -1 >>> 0 === 4294967295 |
x | 0, ~~x | truncate to int32 | only while |x| < 2 ** 31: 1e10 | 0 === 1410065408 |
Math.clz32(x) | leading zero bits of the 32-bit value | 31 - Math.clz32(n) is ⌊log₂ n⌋ for 0 < n < 2 ** 32 |
Math.imul(a, b) | the exact low 32 bits of a * b | a * b | 0 goes wrong once a * b passes 2⁵³ |
x.toString(2) | binary text | sign and magnitude: (-5).toString(2) === "-101"; (-5 >>> 0).toString(2) shows the bits |
parseInt(s, 2) | parse binary | parseInt("1011", 2) === 11 |
| popcount | not built in | the v &= v - 1 loop or the SWAR version below |
bigint bits | &, |, ^, ~, <<, >> | any width; >>> throws TypeError |
&, | and ^ bind more loosely than ===, so n & 1 === 0 means n & (1 === 0). JavaScript
quietly computes n & 0, which is always 0; TypeScript rejects it (TS2363 and TS2367). Write
(n & 1) === 0.
const x = 12;
x & (x - 1); // 8: lowest set bit cleared
x & -x; // 4: lowest set bit alone
1 << 31; // -2147483648
1 << 32; // 1: the count is 32 % 32 = 0
-1 >>> 0; // 4294967295
(-5 >>> 0).toString(2).slice(-8); // "11111011"
31 - Math.clz32(40); // 5: floor(log2(40))
Math.imul(123456789, 987654321); // -67153019
(123456789 * 987654321) | 0; // -67153024: product rounded1/6
x | 0 runs ToInt32: drop the fraction toward 0, keep the low 32 bits, then read bit 31 as the sign. 3.7 | 0 is 3.
The tricks themselves (lowest set bit, submasks, xor cancellation, popcount DP) are in Bit manipulation. Two of them, bit by bit, then popcount:
function popcount(n: number): number {
let count = 0;
// >>> 0 so negative numbers count their 32 bits
for (let v = n >>> 0; v !== 0; v &= v - 1) count++;
return count;
}
// the same with no loop: add bits in pairs, nibbles, bytes
function popcount32(x: number): number {
x -= (x >>> 1) & 0x55555555;
x = (x & 0x33333333) + ((x >>> 2) & 0x33333333);
x = (x + (x >>> 4)) & 0x0f0f0f0f;
return Math.imul(x, 0x01010101) >>> 24;
}
[popcount(13), popcount(-1), popcount32(255)]; // [3, 32, 8]1/9
x = 12 is 0b1100. Its lowest set bit is bit 2.
Bitmasks over n items enumerate every subset, as long as 1 << n stays positive (n up to 30):
const items = ["a", "b", "c"];
const subsets: string[][] = [];
for (let mask = 0; mask < 1 << items.length; mask++) {
subsets.push(items.filter((_, i) => (mask >> i) & 1));
}
subsets.length; // 8
subsets[5]; // ["a", "c"]: mask 0b101Floats
A number is an IEEE 754 double: a sign bit, 11 exponent bits and 52 fraction bits with a hidden
leading 1, so integers up to 2⁵³ are exact and most decimal fractions are not.
(0.1).toString(2).slice(0, 22);
// "0.00011001100110011001"
0.1 + 0.2 === 0.3; // false
Math.abs(0.1 + 0.2 - 0.3) < 1e-9; // true
(1.005).toPrecision(20); // "1.0049999999999998934"| Expression | Result | Why |
|---|---|---|
0.1 + 0.2 | 0.30000000000000004 | neither 0.1 nor 0.2 is exact in binary |
Math.abs(a - b) < 1e-9 | tolerance test | pick the tolerance from the problem's scale |
Number.EPSILON | 2.220446049250313e-16 | 2⁻⁵², the gap after 1: too small as a tolerance for values far from 1 |
1e9 + 0.1 - 1e9 | 0.10000002384185791 | near 10⁹ the gap between doubles is about 1.2e-7 |
10 ** 16 / 3 === (10 ** 16 + 1) / 3 | true | two different quotients, one double |
NaN === NaN | false | test with Number.isNaN(x) |
isNaN("abc"), Number.isNaN("abc") | true, false | the global isNaN converts first |
[NaN].includes(NaN), [NaN].indexOf(NaN) | true, -1 | includes, Set and Map use SameValueZero; indexOf uses === |
0 === -0, Object.is(0, -0) | true, false | 1 / -0 is -Infinity |
0 / 0, Infinity - Infinity | NaN | 1 / 0 is Infinity: division never throws |
Number.MAX_VALUE * 2 | Infinity | Number.MAX_VALUE is 1.7976931348623157e+308 |
Number.MIN_VALUE | 5e-324 | the smallest positive double, not the most negative number |
ten 0.1s added with reduce | 0.9999999999999999 | + rounds at every step |
Math.fround(0.1) | 0.10000000149011612 | the nearest 32-bit float: what a Float32Array stores |
Math.sumPrecise(xs) adds without intermediate rounding, but of the three runtimes here only Bun 1.4
has it, and the TypeScript 6.0 and 7.0 libraries don't declare it yet.
Binary search over floats should loop a fixed number of times. while (hi - lo > eps) never ends
when eps is smaller than the gap between adjacent doubles at that magnitude:
function sqrtBisect(v: number): number {
let lo = 0;
let hi = Math.max(1, v);
for (let i = 0; i < 100; i++) {
const mid = (lo + hi) / 2;
if (mid * mid < v) lo = mid;
else hi = mid;
}
return hi;
}
sqrtBisect(2) === Math.SQRT2; // trueMath & number theory
| Call | Result | Notes |
|---|---|---|
Math.floor, Math.ceil, Math.trunc, Math.round | a number | they can return -0 |
Math.sqrt(n) | correctly rounded | Math.floor(Math.sqrt(n)) is the exact integer root for n < 2 ** 52, then can be one too high |
Math.cbrt(27), Math.hypot(3, 4) | 3, 5 | |
Math.log2(8), Math.log10(1000) | 3, 3 | Math.log(1000) / Math.LN10 is 2.9999999999999996 |
Math.floor(Math.log2(2 ** 50 - 1)) | 50 | wrong (it is 49): use n.toString(2).length - 1 |
Math.floor(Math.log10(999999999999999)) | 15 | wrong (it is 14): count digits with String(n).length |
| gcd, lcm, isqrt, modpow, nCr | not built in | a few lines each (below and in Recipes) |
Math.max(...xs), Math.min(...xs) | RangeError on huge arrays | the limit depends on stack space: about 125 000 arguments on Node 24 and 26 and 640 000 on Bun 1.4 in a test here |
function gcd(a: number, b: number): number {
while (b !== 0) [a, b] = [b, a % b];
return Math.abs(a);
}
const lcm = (a: number, b: number): number =>
(a / gcd(a, b)) * b; // divide first: smaller product
// exact floor(sqrt(n)) for any safe integer n
function isqrt(n: number): number {
let r = Math.floor(Math.sqrt(n));
while (r * r > n) r--;
while ((r + 1) * (r + 1) <= n) r++;
return r;
}
[gcd(12, 18), gcd(0, 5), gcd(-4, 6)]; // [6, 5, 2]
lcm(4, 6); // 12
Math.floor(Math.sqrt(4503599761588224)); // 67108865
isqrt(4503599761588224); // 671088644503599761588224 is 67108865 ** 2 - 1, the smallest number where the plain Math.sqrt floor is
off. lcm can pass 2⁵³ even when its inputs don't; switch to bigint there.
Modular arithmetic
"Return the answer modulo 10⁹ + 7" keeps answers small, but in TypeScript the product of two residues still reaches 10¹⁸, past the 2⁵³ that doubles hold exactly.
| Rule | TypeScript |
|---|---|
| sum | (a + b) % MOD: below 2⁵³, exact |
| difference | ((a - b) % MOD + MOD) % MOD: % can return a negative |
| product | mulmod(a, b, MOD) below, or BigInt |
| power | modPow(b, e, MOD); b ** e % MOD rounds, or overflows to Infinity |
division by b | multiply by modPow(b, MOD - 2, MOD) (Fermat, prime MOD) |
| constant | const MOD = 1_000_000_007; (1e9 + 7 is the same exact integer) |
const MOD = 1_000_000_007;
// exact a * b % m for 0 <= a, b < m < 2 ** 30
function mulmod(a: number, b: number, m: number): number {
const hi = (a * (b >>> 16)) % m; // high part of b
return (hi * 65536 + a * (b & 65535)) % m;
}
function modPow(b: number, e: number, m: number): number {
let result = 1;
b %= m;
// Math.floor(e / 2), not e >> 1: e can pass 2 ** 31
for (; e > 0; e = Math.floor(e / 2)) {
if (e % 2 === 1) result = mulmod(result, b, m);
b = mulmod(b, b, m);
}
return result;
}
mulmod(999_999_999, 999_999_999, MOD); // 64
modPow(2, 1e18, MOD); // 719476260
const inv3 = modPow(3, MOD - 2, MOD);
mulmod(6, inv3, MOD); // 2: "6 / 3" mod p
((3 - 5) % MOD + MOD) % MOD; // 1000000005Every intermediate in mulmod stays below 2⁴⁷: a * (b >>> 16) is under 2⁴⁴, hi * 65536 and
a * (b & 65535) under 2⁴⁶ each. The BigInt version is shorter and works for any modulus:
function modPowBig(b: bigint, e: bigint, m: bigint): bigint {
let result = 1n;
b %= m;
for (; e > 0n; e >>= 1n) {
if (e & 1n) result = (result * b) % m;
b = (b * b) % m;
}
return result;
}
modPowBig(2n, 10n ** 18n, 1_000_000_007n); // 719476260nCounting DP with large answers (paths, ways to climb, subsets) reduces every table cell; sums of
two residues never need mulmod, products always do.
Parsing & formatting
| Call | Result | Notes |
|---|---|---|
Number("42"), +"42", Number(" 42 ") | 42 | the whole string must be a number; surrounding whitespace is fine |
Number(""), Number(" "), Number(null) | 0 | a classic bug: check for empty input first |
Number("12px"), Number(undefined), Number("1_000") | NaN | separators are for literals only |
Number("0x1f"), Number("0b101"), Number("1e3") | 31, 5, 1000 | |
parseInt("12px", 10), parseInt(" -42xyz", 10) | 12, -42 | stops at the first bad character |
parseInt("ff", 16), parseInt("z", 36) | 255, 35 | radix 2 to 36; always pass it |
parseInt(0.0000005) | 5 | the argument becomes the string "5e-7" first |
["1", "2", "3"].map(parseInt) | [1, NaN, NaN] | map passes the index as the radix: use .map(Number) |
parseFloat("3.14abc") | 3.14 | |
(255).toString(2), (255).toString(16) | "11111111", "ff" | any radix 2 to 36 |
(5).toString(2).padStart(8, "0") | "00000101" | fixed width |
(1234.5678).toFixed(2), (1234.5678).toPrecision(6) | "1234.57", "1234.57" | strings, rounded |
String(1e21), String(1e-7) | "1e+21", "1e-7" | exponent form from 10²¹ up and below 10⁻⁶ |
new Intl.NumberFormat("en-US").format(1234567.891) | "1,234,567.891" | grouping; also style: "percent", notation: "compact" |
const n = 9045;
[...String(n)].map(Number); // [9, 0, 4, 5]
const digits: number[] = [];
for (let m = n; m > 0; m = Math.floor(m / 10)) {
digits.push(m % 10); // least significant first
}
digits; // [5, 4, 0, 9]
digits.reduce((s, d) => s + d, 0); // 18
Number([9, 0, 4, 5].join("")); // 9045
["1", "2", "3"].map(Number); // [1, 2, 3]Typed arrays
| Type | Element | Storing a value x |
|---|---|---|
Int32Array | signed 32-bit | ToInt32: 2 ** 31 becomes -2147483648, 1.9 becomes 1, -1.9 becomes -1 |
Uint32Array | unsigned 32-bit | modulo 2³² |
Uint8Array | unsigned 8-bit | modulo 256: 256 becomes 0, -1 becomes 255 |
Uint8ClampedArray | 8-bit, clamped | 300 becomes 255, -5 becomes 0, 1.5 becomes 2, 2.5 becomes 2 (ties to even) |
Float64Array | double | an exact copy of a number |
Float32Array | 32-bit float | Math.fround(x) |
Float16Array | 16-bit float | new in ES2025; Node 24+ and Bun |
BigInt64Array, BigUint64Array | 64-bit integer | bigint values only |
A typed array has a fixed length, starts zero-filled, sorts numerically by default, and has no
push. subarray returns a view on the same memory; slice copies. A million small integers
took about 8 MB of heap as a plain array and 4 MB as an Int32Array on Node 24.
const counts = new Int32Array(26); // zero-filled
for (const ch of "banana") counts[ch.charCodeAt(0) - 97]++;
[counts[0], counts[1], counts[13]]; // [3, 1, 2]
new Int32Array([10, 9, 1, 2]).sort(); // [1, 2, 9, 10]
[10, 9, 1, 2].sort(); // [1, 10, 2, 9]: string order
const cell = new Int32Array(1);
cell[0] = 2 ** 31; // stored through ToInt32
cell[0]; // -2147483648
const buf = new Uint8Array([1, 2, 3, 4]);
const view = buf.subarray(1, 3); // shares memory
view[0] = 9;
buf; // [1, 9, 3, 4]Costs
| Operation | Cost | Notes |
|---|---|---|
+, -, *, /, %, comparisons on number | O(1) | one machine operation on a double or a small integer |
bigint arithmetic | O(digits), allocates | for 10⁹-sized modular products, about 1.5× the time of mulmod (Node 24) |
Math.floor(a / b) | O(1) | the exact quotient for safe integers |
String(n), Number(s), parseInt(s, 10) | O(length) | |
n.toString(2) | O(bits) | |
Math.max(...xs) | O(n), one argument per element | RangeError past the engine's argument limit |
new Int32Array(n) | O(n) to zero-fill | 4 bytes per element; a plain array of small integers took 8 |
DS&A idioms
The integer moves problem solutions use, and the trap in each. The patterns are in the DS&A sheets (linked in the last column).
| Job | TypeScript | Watch out | More |
|---|---|---|---|
| Sentinels | let best = Infinity, let worst = -Infinity | fine with Math.min, Math.max and <; but Infinity | 0 is 0 | |
| Integer division | Math.floor(a / b) or Math.trunc(a / b) | a / b | 0 wraps past 2³¹ | TS gotchas |
| 32-bit overflow | if (r > 2 ** 31 - 1 || r < -(2 ** 31)) return 0; | a number holds the overflowed value exactly, so test before returning | Recipes |
| Midpoint | lo + Math.floor((hi - lo) / 2), or (lo + hi) >>> 1 | >>> 1 only while lo + hi < 2 ** 32; >> 1 breaks at 2³¹ | Binary search |
| Search on the answer | let lo = 0, hi = 1e9; then shrink | integers: exact; floats: a fixed number of iterations | On the answer |
| Parity | (n & 1) === 1, or n % 2 !== 0 | n % 2 === 1 misses negative odd numbers: -7 % 2 is -1 | |
| Wraparound index | ((i % n) + n) % n | % keeps the dividend's sign | |
| Power of two | n > 0 && (n & (n - 1)) === 0 | the parentheses matter: === binds tighter than &; only for n < 2 ** 31 | Bit manipulation |
| Bitmask subsets | for (let m = 0; m < 1 << n; m++), test (m >> i) & 1 | 1 << 31 is negative: n up to 30 | Input size |
| Grid index ↔ cell | r = Math.floor(i / cols), c = i % cols, i = r * cols + c | cols, not rows, is the divisor | Grids as graphs |
| Digits | [...String(Math.abs(n))].map(Number), or % 10 with Math.floor(m / 10) | take Math.abs first: -123 % 10 is -3 | Fast & slow pointers |
| Answers mod p | const MOD = 1_000_000_007, reduce after every step | products need mulmod or BigInt | Modular arithmetic |
| Fast power | modPow(b, e, MOD) | halve e with Math.floor(e / 2) when it may pass 2³¹ | |
| Divisor loops | for (let d = 1; d * d <= n; d++) | ||
| Perfect square | isqrt(n) ** 2 === n | Number.isInteger(Math.sqrt(n)) is safe only below 2⁵² | Math & number theory |
| Missing number | n * (n + 1) / 2 - sum, or xor everything | exact while the sum stays below 2⁵³ | Index as hash |
| Float equality | Math.abs(a - b) < 1e-9 | Number.EPSILON is far too small away from 1 | |
| Floor log₂ | 31 - Math.clz32(n) (32-bit), n.toString(2).length - 1 | Math.floor(Math.log2(2 ** 50 - 1)) is 50, one too high | |
| Sorting numbers | xs.sort((a, b) => a - b) | the default sort compares strings | The default sort trap |
| Counting arrays | new Int32Array(26), index ch.charCodeAt(0) - 97 | already zero-filled | Typed arrays |
| Max of many | a loop, or xs.reduce((a, b) => Math.max(a, b)) | Math.max(...xs) throws past about 125 000 elements on Node |
const cols = 4;
const i = 7;
[Math.floor(i / cols), i % cols]; // [1, 3]
const lo = 2 ** 30;
const hi = 2 ** 30 + 10;
(lo + hi) >> 1; // -1073741819: lo + hi passed 2 ** 31
(lo + hi) >>> 1; // 1073741829
lo + Math.floor((hi - lo) / 2); // 1073741829
const isPow2 = (n: number): boolean =>
n > 0 && (n & (n - 1)) === 0;
[isPow2(64), isPow2(96), isPow2(0)]; // [true, false, false]
-7 % 2 === 1; // false: -7 is odd, but % gives -1
(-7 & 1) === 1; // trueTyping
| Annotation | For |
|---|---|
number, bigint | the two numeric primitives (never Number or BigInt) |
0 | 1, -1 | 0 | 1 | literal unions: bits, signs, comparator results |
readonly [number, number] | a pair: a grid cell, [value, index], an edge |
as const | const DIRS = [[0, 1], [1, 0]] as const keeps the literal tuples |
number & { readonly __brand: "NodeId" } | a branded id, so a node id can't be passed where a weight belongs |
a const object instead of enum | Node 24 and 26 run .ts files by stripping types, and reject enum (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX); TS 5.8+ flags it under erasableSyntaxOnly |
noUncheckedIndexedAccess | xs[i], and counts[i] on a typed array, become number | undefined: check, use ?? 0, or xs[i]! once the bounds are proved |
TypeScript catches bigint mixed with number (TS2365), Math.max(1n) (TS2345) and the
n & 1 === 0 precedence bug (TS2363). It can't see integer versus fraction, safe versus unsafe, or
32-bit wrapping: all of those are number. Number.isInteger and Number.isSafeInteger are not
type guards.
type NodeId = number & { readonly __brand: "NodeId" };
const nodeId = (n: number): NodeId => n as NodeId;
function dist(from: NodeId, to: NodeId): number {
return Math.abs(from - to);
}
const a = nodeId(3);
dist(a, nodeId(7)); // 4
// @ts-expect-error: a plain number is not a NodeId
dist(a, 7);
const DIRS = [[0, 1], [1, 0], [0, -1], [-1, 0]] as const;
type Step = (typeof DIRS)[number]; // readonly [0, 1] | ...
const sign = (x: number): -1 | 0 | 1 =>
x < 0 ? -1 : x > 0 ? 1 : 0;
[sign(-4), sign(0), sign(9)]; // [-1, 0, 1]
const Color = { Red: 0, Green: 1 } as const;
type Color = (typeof Color)[keyof typeof Color]; // 0 | 1
const c: Color = Color.Green; // 1Coming from Python
| Python | TypeScript |
|---|---|
int (exact, unbounded) | number (exact up to 2⁵³) or bigint (10n) |
float | number: the same IEEE 754 double |
a // b | Math.floor(a / b) |
int(a / b) | Math.trunc(a / b) |
a % b (divisor's sign: -7 % 3 == 2) | ((a % b) + b) % b; JS % is Python's math.fmod |
divmod(a, b) | [Math.floor(a / b), ((a % b) + b) % b] |
round(2.5) == 2 (half to even) | Math.round(2.5) === 3 (half toward +∞) |
f"{x:.2f}" | x.toFixed(2) |
int("ff", 16); int("12px") raises | parseInt("ff", 16); parseInt("12px", 10) is 12, Number("12px") is NaN |
bin(x), format(x, "b") | x.toString(2) |
x.bit_count() | no built-in: popcount |
x.bit_length() | 32 - Math.clz32(x) (32-bit), x.toString(2).length |
1 << 31 == 2147483648 | 1 << 31 === -2147483648; use 2 ** 31 |
x & 0xFFFFFFFF | x >>> 0 |
pow(b, e, m), pow(a, -1, m) | modPow(b, e, m), and modPow(a, m - 2, m) for a prime m, from Modular arithmetic; or bigint |
math.gcd, math.isqrt, math.comb | write them: Math & number theory, nCr mod p |
math.isclose(a, b) | Math.abs(a - b) <= tol |
math.inf, math.nan, math.isnan(x) | Infinity, NaN, Number.isNaN(x) |
sys.maxsize | Number.MAX_SAFE_INTEGER is the nearest idea: the exact-integer limit |
sum(xs) (compensated since 3.12) | xs.reduce((a, b) => a + b, 0): plain, rounds at each step |
array.array("i"), bytearray | Int32Array, Uint8Array |
Recipes
Sieve of Eratosthenes
When you need every prime up to n (counting primes, prime factors of many numbers).
function primesUpTo(n: number): number[] {
if (n < 2) return [];
const composite = new Uint8Array(n + 1); // all 0
const primes: number[] = [];
for (let p = 2; p <= n; p++) {
if (composite[p]) continue;
primes.push(p);
for (let m = p * p; m <= n; m += p) composite[m] = 1;
}
return primes;
}
primesUpTo(30);
// [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
primesUpTo(1e6).length; // 78498O(n log log n) time and one byte per number. A boolean[] works too, but a Uint8Array is
allocated zero-filled in one step.
Prime factorization
When you need the prime factors of one number (divisor counts, gcd tricks).
function factorize(n: number): Map<number, number> {
const f = new Map<number, number>();
for (let d = 2; d * d <= n; d += d === 2 ? 1 : 2) {
while (n % d === 0) {
f.set(d, (f.get(d) ?? 0) + 1);
n /= d;
}
}
if (n > 1) f.set(n, (f.get(n) ?? 0) + 1); // a prime
return f;
}
const f = factorize(360); // Map(3) {2 => 3, 3 => 2, 5 => 1}
let divisors = 1;
for (const e of f.values()) divisors *= e + 1;
divisors; // 24O(√n). For many queries up to one limit, sieve the smallest prime factor of every number once instead.
nCr mod p
When a counting problem needs many binomial coefficients modulo a prime.
const MOD = 1_000_000_007;
const N = 100_000;
function mulmod(a: number, b: number, m: number): number {
const hi = (a * (b >>> 16)) % m;
return (hi * 65536 + a * (b & 65535)) % m;
}
function modPow(b: number, e: number, m: number): number {
let r = 1;
for (; e > 0; e = Math.floor(e / 2)) {
if (e % 2 === 1) r = mulmod(r, b, m);
b = mulmod(b, b, m);
}
return r;
}
const fact = new Array<number>(N + 1).fill(1);
for (let i = 1; i <= N; i++) {
fact[i] = mulmod(fact[i - 1], i, MOD);
}
const invFact = new Array<number>(N + 1).fill(1);
invFact[N] = modPow(fact[N], MOD - 2, MOD); // Fermat
for (let i = N; i > 0; i--) {
invFact[i - 1] = mulmod(invFact[i], i, MOD);
}
function nCr(n: number, r: number): number {
if (r < 0 || r > n) return 0;
const top = mulmod(fact[n], invFact[r], MOD);
return mulmod(top, invFact[n - r], MOD);
}
[nCr(5, 2), nCr(100_000, 50_000)]; // [10, 149033233]O(N) setup with a single modular inverse, then O(1) per query.
Reverse Integer with a 32-bit clamp
When a problem written for fixed-width integers wants 0 on overflow.
const INT_MAX = 2 ** 31 - 1;
const INT_MIN = -(2 ** 31);
function reverseInt(x: number): number {
let n = Math.abs(x); // -123 % 10 would be -3
let r = 0;
while (n > 0) {
r = r * 10 + (n % 10);
n = Math.floor(n / 10);
}
if (x < 0) r = -r;
return r < INT_MIN || r > INT_MAX ? 0 : r;
}
[reverseInt(-123), reverseInt(120)]; // [-321, 21]
reverseInt(1534236469); // 0: 9646324351 overflowsr can hold the overflowed value exactly (it stays far below 2⁵³), so the check after the loop is
enough. In C or Java the multiply itself would wrap and the check would have to come first.
Add binary strings
When two binary (or hex) strings are too long for a number.
const addBinary = (a: string, b: string): string =>
(BigInt(`0b${a}`) + BigInt(`0b${b}`)).toString(2);
addBinary("1011", "11"); // "1110"
addBinary("1".repeat(100), "1").length; // 101
(255n).toString(16); // "ff"
BigInt("0xff"); // 255nBigInt parses 0b, 0o and 0x prefixes, and toString(radix) writes any base from 2 to 36.
The digit-by-digit carry loop is the answer when the interviewer rules BigInt out.
Counting bits
When you need the popcount of every number from 0 to n, in O(n).
function countBits(n: number): number[] {
const bits = new Array<number>(n + 1).fill(0);
for (let i = 1; i <= n; i++) {
bits[i] = bits[i >> 1] + (i & 1); // drop the last bit
}
return bits;
}
countBits(5); // [0, 1, 1, 2, 1, 2]i >> 1 has every bit of i except the last one, and was computed earlier in the loop.
Fibonacci mod m by matrix power
When n is huge (10¹⁸) and the recurrence is linear: square the matrix instead of looping n
times.
type Mat = readonly [number, number, number, number];
function mulmod(a: number, b: number, m: number): number {
const hi = (a * (b >>> 16)) % m;
return (hi * 65536 + a * (b & 65535)) % m;
}
function matMul(x: Mat, y: Mat, m: number): Mat {
const [a, b, c, d] = x; // [[a, b], [c, d]]
const [e, f, g, h] = y;
const mm = (p: number, q: number, r: number, s: number) =>
(mulmod(p, q, m) + mulmod(r, s, m)) % m;
return [
mm(a, e, b, g),
mm(a, f, b, h),
mm(c, e, d, g),
mm(c, f, d, h),
];
}
function fib(n: number, m = 1_000_000_007): number {
let result: Mat = [1, 0, 0, 1]; // identity
let base: Mat = [1, 1, 1, 0];
for (; n > 0; n = Math.floor(n / 2)) {
if (n % 2 === 1) result = matMul(result, base, m);
base = matMul(base, base, m);
}
return result[1]; // base ** n = [[F(n+1), F(n)], ...]
}
Array.from({ length: 10 }, (_, i) => fib(i));
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
fib(1e18); // 209783453O(log n) matrix products. Any recurrence f(n) = c1·f(n−1) + … + ck·f(n−k) works with a k × k
matrix.
Exact slopes: points on a line
When grouping points by direction; float slopes merge or split lines at large coordinates.
type Pt = readonly [number, number];
function gcd(a: number, b: number): number {
while (b !== 0) [a, b] = [b, a % b];
return Math.abs(a);
}
function direction(dx: number, dy: number): string {
const g = gcd(dx, dy); // > 0 unless both are 0
dx /= g;
dy /= g;
if (dx < 0 || (dx === 0 && dy < 0)) {
dx = -dx; // one sign per direction
dy = -dy;
}
return `${dx},${dy}`; // `${-0}` is "0"
}
function maxPoints(pts: readonly Pt[]): number {
let best = Math.min(pts.length, 1);
pts.forEach(([x1, y1], i) => {
const dirs = new Map<string, number>();
for (const [x2, y2] of pts.slice(i + 1)) {
const key = direction(x2 - x1, y2 - y1);
const c = (dirs.get(key) ?? 0) + 1;
dirs.set(key, c);
best = Math.max(best, c + 1);
}
});
return best;
}
const pts: Pt[] = [
[1, 1], [3, 2], [5, 3], [4, 1], [2, 3], [1, 4],
];
maxPoints(pts); // 4O(n²) pairs. The key is a string because Map compares arrays by reference: [1, 2] and
[1, 2] would be two different keys. Assumes distinct points.
References
- MDN: Number (opens in a new tab):
MAX_SAFE_INTEGER,EPSILON,isInteger,toFixed,toString(radix) - MDN: BigInt (opens in a new tab): operators,
asIntN,asUintN, what throws - MDN: Math (opens in a new tab):
trunc,clz32,imul,fround,hypot - MDN: Remainder (%) (opens in a new tab) and Unsigned right shift (>>>) (opens in a new tab)
- MDN: parseInt (opens in a new tab): radix rules and the
parseInt(0.0000005)trap - MDN: JavaScript typed arrays (opens in a new tab): buffers, views, element conversions
- ECMAScript: ToInt32 (opens in a new tab), Number::remainder (opens in a new tab) and Number.prototype.toFixed (opens in a new tab): the exact rules
- MDN: Math.sumPrecise (opens in a new tab): exact summation, and which engines have it
- TypeScript: noUncheckedIndexedAccess (opens in a new tab) and erasableSyntaxOnly (opens in a new tab)
- Node.js: Running TypeScript natively (opens in a new tab): type stripping and what it rejects
- V8: BigInt (opens in a new tab): arbitrary-precision integers in JavaScript
- cp-algorithms: Sieve of Eratosthenes (opens in a new tab), binary exponentiation (opens in a new tab) and modular inverse (opens in a new tab)