Math #
Mathematical operations appear in almost every application — calculating discounts, rounding prices, generating random IDs, normalizing data for charts, or computing the distance between two coordinates. TypeScript inherits JavaScript’s built-in Math object, which provides common mathematical constants and dozens of ready-to-use functions. Beyond that, there are several traps to understand — especially floating point, which can produce unexpected results when working with money or high-precision numbers. This article covers the full power of Math, the characteristics of the number type in JavaScript, BigInt for integers beyond the safe limit, and calculation patterns that often appear in real applications.
The Number Type in TypeScript #
Before diving into Math, it’s important to understand how TypeScript and JavaScript represent numbers. Every number in JavaScript — both integers and decimals — is stored in the IEEE 754 double-precision floating-point 64-bit format. This isn’t a TypeScript weakness; it’s the same standard used by Python, Java, and almost every modern language.
// all of these are the same number type
const integer: number = 42;
const desimal: number = 3.14;
const negatif: number = -100;
const besar: number = 1_000_000; // underscore as a thousands separator (ES2021)
const hex: number = 0xFF; // 255 in hexadecimal
const oktal: number = 0o77; // 63 in octal
const biner: number = 0b1010; // 10 in binary
const saintifik: number = 1.5e3; // 1500
// special values
console.log(Infinity); // infinity
console.log(-Infinity); // negative infinity
console.log(NaN); // Not a Number — the result of an invalid operation
// check special values
console.log(isFinite(Infinity)); // false
console.log(isFinite(42)); // true
console.log(isNaN(NaN)); // true
console.log(isNaN("abc")); // true — converts to number first!
// ANTI-PATTERN: using the coercive global isNaN
console.log(isNaN("abc")); // ✗ true — "abc" is converted to NaN first
// CORRECT: use the strict Number.isNaN — no conversion
console.log(Number.isNaN("abc")); // ✓ false — "abc" is not NaN, it's a string
console.log(Number.isNaN(NaN)); // ✓ true
// safe integer limits
console.log(Number.MAX_SAFE_INTEGER); // 9_007_199_254_740_991 (2^53 - 1)
console.log(Number.MIN_SAFE_INTEGER); // -9_007_199_254_740_991
// numbers beyond the safe limit lose precision
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true — they're the same!
// check whether an integer is safe
console.log(Number.isSafeInteger(42)); // true
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER)); // true
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)); // false
Math Constants #
The Math object provides the mathematical constants often needed, all in full floating-point precision.
console.log(Math.PI); // 3.141592653589793 — π
console.log(Math.E); // 2.718281828459045 — Euler's number
console.log(Math.SQRT2); // 1.4142135623730951 — square root of 2
console.log(Math.SQRT1_2); // 0.7071067811865476 — square root of 1/2
console.log(Math.LN2); // 0.6931471805599453 — natural logarithm of 2
console.log(Math.LN10); // 2.302585092994046 — natural logarithm of 10
console.log(Math.LOG2E); // 1.4426950408889634 — log base 2 of E
console.log(Math.LOG10E); // 0.4342944819032518 — log base 10 of E
// example usage: circle area
function luasLingkaran(jariJari: number): number {
return Math.PI * jariJari ** 2;
}
// example usage: circle circumference
function kelilingLingkaran(jariJari: number): number {
return 2 * Math.PI * jariJari;
}
console.log(luasLingkaran(5).toFixed(4)); // "78.5398"
console.log(kelilingLingkaran(5).toFixed(4)); // "31.4159"
Rounding #
Rounding is the operation most often applied incorrectly. JavaScript provides four rounding functions with different behaviors — it’s important to choose the right one for the right context.
// Math.round — round to the nearest integer (0.5 rounds up)
console.log(Math.round(4.4)); // 4
console.log(Math.round(4.5)); // 5
console.log(Math.round(4.6)); // 5
console.log(Math.round(-4.5)); // -4 (not -5 — always rounds up, not toward zero)
// Math.floor — round down (toward -∞)
console.log(Math.floor(4.9)); // 4
console.log(Math.floor(4.1)); // 4
console.log(Math.floor(-4.1)); // -5 (down, not toward zero)
// Math.ceil — round up (toward +∞)
console.log(Math.ceil(4.1)); // 5
console.log(Math.ceil(4.9)); // 5
console.log(Math.ceil(-4.9)); // -4 (up, not toward zero)
// Math.trunc — truncate the decimals (toward 0)
console.log(Math.trunc(4.9)); // 4
console.log(Math.trunc(-4.9)); // -4 (toward zero, not down)
console.log(Math.trunc(4.1)); // 4
// Math.floor vs Math.trunc for negatives
console.log(Math.floor(-4.1)); // -5 ← smaller
console.log(Math.trunc(-4.1)); // -4 ← toward zero
Comparison table for choosing the right rounding function:
| Value | round | floor | ceil | trunc |
|---|---|---|---|---|
| 4.1 | 4 | 4 | 5 | 4 |
| 4.5 | 5 | 4 | 5 | 4 |
| 4.9 | 5 | 4 | 5 | 4 |
| -4.1 | -4 | -5 | -4 | -4 |
| -4.5 | -4 | -5 | -4 | -4 |
| -4.9 | -5 | -5 | -4 | -4 |
Rounding to N Decimals #
Math.round only rounds to an integer. For rounding to N decimals, there are several approaches:
// ANTI-PATTERN: using toFixed() and converting back to a number
function bulatkanSalah(angka: number, desimal: number): number {
return parseFloat(angka.toFixed(desimal)); // ✗ can still have floating point errors
}
// a more accurate approach with a multiplier factor
function bulatkan(angka: number, desimal: number): number {
const faktor = 10 ** desimal;
return Math.round((angka + Number.EPSILON) * faktor) / faktor;
}
console.log(bulatkan(1.005, 2)); // 1.01 — correct
console.log(bulatkan(1.555, 2)); // 1.56 — correct
console.log(bulatkan(3.14159, 3)); // 3.142
// round down with N decimals
function bulatkanBawah(angka: number, desimal: number): number {
const faktor = 10 ** desimal;
return Math.floor(angka * faktor) / faktor;
}
// round up with N decimals
function bulatkanAtas(angka: number, desimal: number): number {
const faktor = 10 ** desimal;
return Math.ceil(angka * faktor) / faktor;
}
console.log(bulatkanBawah(1.559, 2)); // 1.55
console.log(bulatkanAtas(1.551, 2)); // 1.56
Extreme Values and Comparison #
// Math.max and Math.min — the largest and smallest of the arguments
console.log(Math.max(1, 5, 3, 9, 2)); // 9
console.log(Math.min(1, 5, 3, 9, 2)); // 1
// with arrays — use the spread operator
const angka = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max(...angka)); // 9
console.log(Math.min(...angka)); // 1
// ANTI-PATTERN: spreading a large array into Math.max
// for arrays with thousands of elements, spread can cause a stack overflow
const arrayBesar = Array.from({ length: 100_000 }, (_, i) => i);
// Math.max(...arrayBesar) ✗ — can crash with "Maximum call stack size exceeded"
// CORRECT: use reduce for large arrays
const maksimal = arrayBesar.reduce((maks, val) => (val > maks ? val : maks), -Infinity);
const minimal = arrayBesar.reduce((min, val) => (val < min ? val : min), Infinity);
// Math.abs — absolute value
console.log(Math.abs(-42)); // 42
console.log(Math.abs(42)); // 42
console.log(Math.abs(-3.14)); // 3.14
// Math.sign — the sign of a number: -1, 0, or 1
console.log(Math.sign(-5)); // -1
console.log(Math.sign(0)); // 0
console.log(Math.sign(5)); // 1
// Math.clamp — limit a value to a range (no built-in, make your own)
function clamp(nilai: number, min: number, max: number): number {
return Math.min(Math.max(nilai, min), max);
}
console.log(clamp(150, 0, 100)); // 100 — too big, clipped to max
console.log(clamp(-10, 0, 100)); // 0 — too small, clipped to min
console.log(clamp(50, 0, 100)); // 50 — within range, unchanged
// use case: clamp a slider or progress bar value
const progress = clamp(volume, 0, 100);
const volume = 150;
Powers, Roots, and Logarithms #
// Math.pow — power (alternative: the ** operator)
console.log(Math.pow(2, 10)); // 1024
console.log(2 ** 10); // 1024 — more concise
// Math.sqrt — square root
console.log(Math.sqrt(16)); // 4
console.log(Math.sqrt(2)); // 1.4142135623730951
// Math.cbrt — cube root
console.log(Math.cbrt(27)); // 3
console.log(Math.cbrt(-8)); // -2
// Nth root — use a fractional power
function akarPangkat(angka: number, pangkat: number): number {
return angka ** (1 / pangkat);
}
console.log(akarPangkat(16, 4)); // 2 (4th root of 16)
console.log(akarPangkat(32, 5)); // 2 (5th root of 32)
// Math.log — natural logarithm (base e)
console.log(Math.log(Math.E)); // 1
console.log(Math.log(1)); // 0
console.log(Math.log(Math.E ** 3)); // 3
// Math.log2 — base 2 logarithm
console.log(Math.log2(1024)); // 10
console.log(Math.log2(256)); // 8
// Math.log10 — base 10 logarithm
console.log(Math.log10(1000)); // 3
console.log(Math.log10(100)); // 2
// base N logarithm
function logN(angka: number, basis: number): number {
return Math.log(angka) / Math.log(basis);
}
console.log(logN(81, 3)); // 4 (log base 3 of 81)
console.log(logN(625, 5)); // 4 (log base 5 of 625)
// Math.hypot — hypotenuse length (root of the sum of squares)
console.log(Math.hypot(3, 4)); // 5 (Pythagorean theorem: √(9+16))
console.log(Math.hypot(5, 12)); // 13
// distance between two 2D points
function jarak2D(x1: number, y1: number, x2: number, y2: number): number {
return Math.hypot(x2 - x1, y2 - y1);
}
// distance between two 3D points
function jarak3D(
x1: number, y1: number, z1: number,
x2: number, y2: number, z2: number
): number {
return Math.hypot(x2 - x1, y2 - y1, z2 - z1);
}
console.log(jarak2D(0, 0, 3, 4)); // 5
console.log(jarak3D(0, 0, 0, 1, 2, 2)); // 3
Trigonometry #
All trigonometric functions in Math use radians, not degrees. This is a very common source of errors.
// degrees ↔ radians conversion
function keRadian(derajat: number): number {
return (derajat * Math.PI) / 180;
}
function keDerajat(radian: number): number {
return (radian * 180) / Math.PI;
}
// ANTI-PATTERN: using degrees directly without conversion
console.log(Math.sin(90)); // ✗ 0.894 — not 1! because 90 is treated as radians
// CORRECT: convert to radians first
console.log(Math.sin(keRadian(90))); // ✓ 1
console.log(Math.cos(keRadian(0))); // ✓ 1
console.log(Math.cos(keRadian(180))); // ✓ -1 (with a little floating point noise)
// basic trigonometric functions
console.log(Math.sin(keRadian(30))); // 0.5
console.log(Math.cos(keRadian(60))); // 0.5
console.log(Math.tan(keRadian(45))); // 1 (approximately — some floating point noise)
// inverse functions
console.log(keDerajat(Math.asin(1))); // 90
console.log(keDerajat(Math.acos(0.5))); // 60
console.log(keDerajat(Math.atan(1))); // 45
// Math.atan2 — the angle from coordinates (x, y) — more useful than atan
// returns the angle in radians from the positive X axis to the point (x, y)
console.log(keDerajat(Math.atan2(1, 1))); // 45
console.log(keDerajat(Math.atan2(1, -1))); // 135
console.log(keDerajat(Math.atan2(-1, -1))); // -135
// example: wind direction from U and V components
function arahAngin(u: number, v: number): number {
const arahRad = Math.atan2(v, u);
const arahDerajat = keDerajat(arahRad);
return (arahDerajat + 360) % 360; // normalize to 0–360
}
// example: point position on a circle
function titikPadaLingkaran(
pusatX: number,
pusatY: number,
jariJari: number,
sudutDerajat: number
): { x: number; y: number } {
const sudutRad = keRadian(sudutDerajat);
return {
x: pusatX + jariJari * Math.cos(sudutRad),
y: pusatY + jariJari * Math.sin(sudutRad),
};
}
// points at 3 o'clock (0°), 12 o'clock (90°), 9 o'clock (180°), 6 o'clock (270°) on an analog clock
console.log(titikPadaLingkaran(0, 0, 10, 0)); // { x: 10, y: 0 }
console.log(titikPadaLingkaran(0, 0, 10, 90)); // { x: ~0, y: 10 }
Random Numbers #
Math.random() produces a random number between 0 (inclusive) and 1 (exclusive). From here you can build various random generators.
// Math.random() — [0, 1)
console.log(Math.random()); // e.g. 0.7234...
// a random integer in the inclusive range [min, max]
function acakInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(acakInt(1, 6)); // dice: 1–6
console.log(acakInt(0, 100)); // random percentage
// a random float in the range [min, max)
function acakFloat(min: number, max: number): number {
return Math.random() * (max - min) + min;
}
console.log(acakFloat(1.0, 5.0)); // e.g. 3.4521...
// pick a random element from an array
function pilihanAcak<T>(arr: T[]): T {
if (arr.length === 0) throw new Error("Array tidak boleh kosong");
return arr[Math.floor(Math.random() * arr.length)];
}
console.log(pilihanAcak(["batu", "gunting", "kertas"]));
// pick several random elements without duplicates
function sampelAcak<T>(arr: T[], jumlah: number): T[] {
if (jumlah > arr.length) throw new Error("Jumlah sampel melebihi panjang array");
const salinan = [...arr];
const hasil: T[] = [];
for (let i = 0; i < jumlah; i++) {
const indeksAcak = Math.floor(Math.random() * (salinan.length - i));
hasil.push(salinan[indeksAcak]);
// swap with the last not-yet-picked element
[salinan[indeksAcak], salinan[salinan.length - 1 - i]] = [
salinan[salinan.length - 1 - i],
salinan[indeksAcak],
];
}
return hasil;
}
// a random boolean with a given probability
function acakBool(probabilitas: number = 0.5): boolean {
return Math.random() < probabilitas;
}
console.log(acakBool(0.3)); // true 30% of the time
// shuffle an array (Fisher-Yates shuffle)
function kocok<T>(arr: T[]): T[] {
const hasil = [...arr]; // copy so the original isn't mutated
for (let i = hasil.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[hasil[i], hasil[j]] = [hasil[j], hasil[i]];
}
return hasil;
}
console.log(kocok([1, 2, 3, 4, 5])); // e.g. [3, 1, 5, 2, 4]
Math.random()is not a cryptographically secure random generator — it’s not safe for tokens, passwords, or OTPs. For security purposes, usecrypto.getRandomValues()(browser) orcrypto.randomBytes()/crypto.randomUUID()(Node.js).import { randomBytes, randomUUID } from "crypto"; // a random 32-byte token in hex const token = randomBytes(32).toString("hex"); // UUID v4 const id = randomUUID(); // "550e8400-e29b-41d4-a716-446655440000"
The Floating Point Problem #
Floating point is the source of bugs that most often surprise developers coming from other languages. Understanding its characteristics is a must, especially when working with money values.
// the famous floating point precision problem
console.log(0.1 + 0.2); // 0.30000000000000004 — not 0.3!
console.log(0.1 + 0.2 === 0.3); // false
// ANTI-PATTERN: comparing floating point directly
if (0.1 + 0.2 === 0.3) { // ✗ never true because of floating point
console.log("sama");
}
// CORRECT: compare with a tolerance (epsilon)
function hampirSama(a: number, b: number, epsilon = Number.EPSILON): boolean {
return Math.abs(a - b) < epsilon;
}
console.log(hampirSama(0.1 + 0.2, 0.3)); // true
// for general purposes, a relative tolerance is safer
function hampirSamaRelatif(
a: number,
b: number,
toleransi = 1e-9
): boolean {
if (a === b) return true; // handle the 0 === 0 case
return Math.abs(a - b) / Math.max(Math.abs(a), Math.abs(b)) < toleransi;
}
Money Calculations — Don’t Use Floats #
// ANTI-PATTERN: calculating prices with floats
function hitungTotalSalah(hargaSatuan: number, qty: number, diskon: number): number {
const subtotal = hargaSatuan * qty;
const nilaiDiskon = subtotal * (diskon / 100);
return subtotal - nilaiDiskon; // ✗ the result can be like 89999.99999999999
}
// CORRECT: use integers (the smallest unit) for money calculations
// store all values in the smallest unit then convert at the end
class Uang {
private sen: bigint; // use BigInt to avoid overflow
constructor(rupiah: number) {
// multiply by 100 to convert to the smallest unit, round first to avoid noise
this.sen = BigInt(Math.round(rupiah * 100));
}
tambah(lain: Uang): Uang {
const hasil = new Uang(0);
hasil.sen = this.sen + lain.sen;
return hasil;
}
kurang(lain: Uang): Uang {
const hasil = new Uang(0);
hasil.sen = this.sen - lain.sen;
return hasil;
}
kali(faktor: number): Uang {
const hasil = new Uang(0);
hasil.sen = BigInt(Math.round(Number(this.sen) * faktor));
return hasil;
}
toRupiah(): number {
return Number(this.sen) / 100;
}
toFormatIDR(): string {
return this.toRupiah().toLocaleString("id-ID", {
style: "currency",
currency: "IDR",
minimumFractionDigits: 0,
});
}
}
// usage
const harga = new Uang(15000);
const diskon = harga.kali(0.1); // 10%
const total = harga.kurang(diskon);
console.log(total.toFormatIDR()); // "Rp 13.500"
BigInt — Unlimited Integers #
BigInt is a primitive data type for integers beyond Number.MAX_SAFE_INTEGER. Unlike number, BigInt has no floating point precision at all — every integer is represented exactly.
// BigInt declaration
const besar = 9_007_199_254_740_991n; // the 'n' suffix
const dariFungsi = BigInt("9007199254740991");
const dariNumber = BigInt(42);
// arithmetic operations — same operators, but must be BigInt on both sides
console.log(9_007_199_254_740_991n + 1n); // 9007199254740992n — full precision
console.log(2n ** 64n); // 18446744073709551616n — far beyond MAX_SAFE_INTEGER
console.log(100n / 3n); // 33n — integer division (no decimals)
// ANTI-PATTERN: mixing BigInt and number
console.log(1n + 1); // ✗ TypeError: Cannot mix BigInt and other types
// CORRECT: explicit conversion
console.log(1n + BigInt(1)); // ✓ 2n
console.log(Number(10n)); // ✓ 10 — be careful: can lose precision for large BigInts
// comparison — can be compared with numbers using == (not ===)
console.log(42n == 42); // true (loose equality)
console.log(42n === 42); // false (strict equality — different types)
console.log(42n > 10); // true — cross-type comparison is allowed
// BigInt doesn't support Math.*
// Math.sqrt(4n) ✗ — TypeError
// BigInt square root (manual implementation)
function sqrtBigInt(n: bigint): bigint {
if (n < 0n) throw new Error("Tidak bisa akar kuadrat bilangan negatif");
if (n < 2n) return n;
let x = n;
let y = (x + 1n) / 2n;
while (y < x) {
x = y;
y = (x + n / x) / 2n;
}
return x;
}
console.log(sqrtBigInt(144n)); // 12n
console.log(sqrtBigInt(2n ** 128n)); // 18446744073709551616n
// use case: unique IDs from timestamp + random (more than MAX_SAFE_INTEGER)
function generateIdBesar(): bigint {
const waktu = BigInt(Date.now()); // millisecond timestamp
const acak = BigInt(Math.floor(Math.random() * 1_000_000));
return waktu * 1_000_000n + acak;
}
Calculation Patterns in Real Applications #
Basic Statistics #
function statistik(data: number[]): {
min: number;
max: number;
jumlah: number;
rerata: number;
median: number;
varians: number;
stdDeviasi: number;
} {
if (data.length === 0) throw new Error("Data tidak boleh kosong");
const terurut = [...data].sort((a, b) => a - b);
const n = data.length;
const jumlah = data.reduce((acc, val) => acc + val, 0);
const rerata = jumlah / n;
// median
const tengah = Math.floor(n / 2);
const median =
n % 2 === 0
? (terurut[tengah - 1] + terurut[tengah]) / 2
: terurut[tengah];
// population variance
const varians =
data.reduce((acc, val) => acc + (val - rerata) ** 2, 0) / n;
return {
min: terurut[0],
max: terurut[n - 1],
jumlah,
rerata: bulatkan(rerata, 4),
median,
varians: bulatkan(varians, 4),
stdDeviasi: bulatkan(Math.sqrt(varians), 4),
};
}
function bulatkan(angka: number, desimal: number): number {
const faktor = 10 ** desimal;
return Math.round((angka + Number.EPSILON) * faktor) / faktor;
}
const nilaiUjian = [75, 88, 92, 68, 95, 78, 85, 90, 72, 88];
console.log(statistik(nilaiUjian));
// { min: 68, max: 95, jumlah: 831, rerata: 83.1, median: 86.5, ... }
Normalization and Scaling #
// min-max normalization — scale values to the range [0, 1]
function normalisasiMinMax(data: number[]): number[] {
const min = Math.min(...data);
const max = Math.max(...data);
const rentang = max - min;
if (rentang === 0) return data.map(() => 0); // all values are the same
return data.map((val) => (val - min) / rentang);
}
// normalize to a specific range [targetMin, targetMax]
function normalisasiKe(
data: number[],
targetMin: number,
targetMax: number
): number[] {
const normalized = normalisasiMinMax(data);
const targetRentang = targetMax - targetMin;
return normalized.map((val) => val * targetRentang + targetMin);
}
// linear interpolation — a value between two points
function lerp(mulai: number, akhir: number, t: number): number {
return mulai + (akhir - mulai) * clamp(t, 0, 1);
}
function clamp(nilai: number, min: number, max: number): number {
return Math.min(Math.max(nilai, min), max);
}
console.log(lerp(0, 100, 0.25)); // 25
console.log(lerp(0, 100, 0.75)); // 75
console.log(lerp(10, 20, 0.5)); // 15
// percentage change
function persentasePerubahan(lama: number, baru: number): number {
if (lama === 0) return baru === 0 ? 0 : Infinity;
return bulatkan(((baru - lama) / Math.abs(lama)) * 100, 2);
}
console.log(persentasePerubahan(100, 120)); // 20
console.log(persentasePerubahan(100, 80)); // -20
console.log(persentasePerubahan(100, 100)); // 0
Geographic Distance — the Haversine Formula #
// calculate the distance between two GPS coordinates in kilometers
function jarakHaversine(
lat1: number, lon1: number,
lat2: number, lon2: number
): number {
const R = 6371; // Earth's radius in kilometers
const dLat = keRadian(lat2 - lat1);
const dLon = keRadian(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(keRadian(lat1)) *
Math.cos(keRadian(lat2)) *
Math.sin(dLon / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return bulatkan(R * c, 3); // in kilometers
}
function keRadian(derajat: number): number {
return (derajat * Math.PI) / 180;
}
// example: the distance from Jakarta to Surabaya
const jarakJktSby = jarakHaversine(
-6.2088, 106.8456, // Jakarta
-7.2575, 112.7521 // Surabaya
);
console.log(`Jarak Jakarta–Surabaya: ${jarakJktSby} km`); // ~664 km
Summary #
- Every number in TypeScript is IEEE 754 double-precision — integers and decimals use the same representation, so precision is limited to 15–17 significant digits.
- Use
Number.isNaN()notisNaN()— the globalisNaN()performs a type conversion first that can produce unexpected results.Math.trunc()differs fromMath.floor()for negative numbers —trunc(-4.9)gives-4, whilefloor(-4.9)gives-5.- Add
Number.EPSILONwhen rounding to N decimals — prevents wrong results from floating point noise like1.005being represented as1.00499....- Don’t use
Math.max(...arrayBesar)— spreading an array with more than ~100,000 elements can cause a stack overflow; usereduceinstead.- All trigonometric functions use radians — always convert degrees to radians with
(derajat * Math.PI) / 180before callingsin,cos, ortan.Math.random()isn’t cryptographic — for tokens, passwords, or OTPs, usecrypto.randomBytes()orcrypto.randomUUID()from Node.js’scryptomodule.- Don’t calculate money with floats — store values in the smallest unit (cents) as integers or use BigInt, then convert to a display format at the end.
BigIntfor numbers beyondNumber.MAX_SAFE_INTEGER— don’t mixBigIntandnumberin one operation without explicit conversion; they’re not directly compatible.