Domina BigInt de JavaScript para computación precisa de enteros a gran escala. Explora su sintaxis, casos de uso en criptografía y finanzas, y supera errores comunes como la serialización JSON.
JavaScript BigInt: Una Guía Completa para la Computación con Números Grandes
Durante muchos años, los desarrolladores de JavaScript se enfrentaron a una limitación silenciosa pero significativa: la capacidad nativa del lenguaje para manejar números. Aunque perfectamente adecuada para cálculos cotidianos, el tipo Number
de JavaScript fallaba al enfrentarse a los enteros verdaderamente masivos requeridos en campos como la criptografía, la computación científica y los sistemas de datos modernos. Esto llevaba a un mundo de soluciones alternativas, bibliotecas de terceros y errores de precisión sutiles y difíciles de depurar.
Esa era ha terminado. La introducción de BigInt como un tipo primitivo nativo de JavaScript ha revolucionado la forma en que trabajamos con números grandes. Proporciona una manera robusta, ergonómica y eficiente de realizar aritmética de enteros de precisión arbitraria, directamente dentro del lenguaje.
Esta guía completa está dirigida a desarrolladores de todo el mundo. Profundizaremos en el "por qué, qué y cómo" de BigInt. Ya sea que estés creando una aplicación financiera, interactuando con una blockchain, o simplemente tratando de entender por qué tu ID único grande de una API se está comportando de manera extraña, este artículo te equipará con el conocimiento para dominar BigInt.
El Problema: Los Límites del Tipo Number de JavaScript
Antes de poder apreciar la solución, debemos comprender completamente el problema. JavaScript solo ha tenido un tipo de número durante la mayor parte de su historia: el tipo Number
. Internamente, se representa como un número de punto flotante de doble precisión de 64 bits IEEE 754. Este formato es excelente para representar una amplia gama de valores, incluidos los decimales, pero tiene una limitación crítica cuando se trata de enteros.
Conoce MAX_SAFE_INTEGER
Debido a su representación de punto flotante, existe un límite para el tamaño de un entero que se puede representar con precisión perfecta. Este límite se expone a través de una constante: Number.MAX_SAFE_INTEGER
.
Su valor es 253 - 1, que es 9.007.199.254.740.991. Llamémoslo nueve cuatrillones para abreviar.
Cualquier entero dentro del rango de -Number.MAX_SAFE_INTEGER
a +Number.MAX_SAFE_INTEGER
se considera un "entero seguro". Esto significa que puede representarse exactamente y compararse correctamente. Pero, ¿qué sucede cuando salimos de este rango?
Veámoslo en acción:
const maxSafe = Number.MAX_SAFE_INTEGER;
console.log(maxSafe); // 9007199254740991
// Sumémosle 1
console.log(maxSafe + 1); // 9007199254740992 - Esto parece correcto
// Sumemos otro 1
console.log(maxSafe + 2); // 9007199254740992 - ¡Ups! Resultado incorrecto.
// Empeora
console.log(maxSafe + 3); // 9007199254740994 - ¿Espera, qué?
console.log(maxSafe + 4); // 9007199254740996 - ¡Está saltando números!
// La comprobación de igualdad también falla
console.log(maxSafe + 1 === maxSafe + 2); // true - ¡Esto es matemáticamente incorrecto!
Como puedes ver, una vez que superamos Number.MAX_SAFE_INTEGER
, JavaScript ya no puede garantizar la precisión de nuestros cálculos. La representación del número comienza a tener huecos, lo que genera errores de redondeo y resultados incorrectos. Esto es una pesadilla para las aplicaciones que exigen precisión con enteros grandes.
Las Soluciones Alternativas Antiguas
Durante años, la comunidad global de desarrolladores recurrió a bibliotecas externas para resolver este problema. Bibliotecas como bignumber.js
, decimal.js
y long.js
se convirtieron en herramientas estándar. Funcionaban representando números grandes como cadenas o arreglos de dígitos e implementando operaciones aritméticas en software.
Aunque efectivas, estas bibliotecas venían con concesiones:
- Sobrecarga de Rendimiento: Las operaciones eran significativamente más lentas que los cálculos de números nativos.
- Tamaño del Paquete: Añadían peso a los paquetes de aplicaciones, una preocupación para el rendimiento web.
- Sintaxis Diferente: Los desarrolladores tenían que usar métodos de objeto (por ejemplo,
a.add(b)
) en lugar de operadores aritméticos estándar (a + b
), lo que hacía el código menos intuitivo.
Presentando BigInt: La Solución Nativa
BigInt se introdujo en ES2020 para resolver este problema de forma nativa. Un BigInt
es un nuevo tipo primitivo en JavaScript que proporciona una forma de representar números enteros mayores que 253 - 1.
La característica clave de BigInt es que su tamaño no está fijo. Puede representar enteros arbitrariamente grandes, limitado solo por la memoria disponible en el sistema host. Esto elimina por completo los problemas de precisión que vimos con el tipo Number
.
Cómo Crear un BigInt
Hay dos formas principales de crear un BigInt:
- Añadiendo `n` a un literal entero: Este es el método más simple y común.
- Usando la función constructora `BigInt()`: Esto es útil al convertir un valor de otro tipo, como una cadena o un número.
Así es como se ven en código:
// 1. Usando el sufijo 'n'
const myFirstBigInt = 900719925474099199n;
const anotherBigInt = 123456789012345678901234567890n;
// 2. Usando el constructor BigInt()
const fromString = BigInt("98765432109876543210");
const fromNumber = BigInt(100);
// Puedes comprobar el tipo
console.log(typeof myFirstBigInt); // "bigint"
console.log(typeof 100); // "number"
Con BigInt, nuestro cálculo anterior que fallaba ahora funciona perfectamente:
const maxSafePlusOne = BigInt(Number.MAX_SAFE_INTEGER) + 1n;
const maxSafePlusTwo = BigInt(Number.MAX_SAFE_INTEGER) + 2n;
console.log(maxSafePlusOne.toString()); // "9007199254740992"
console.log(maxSafePlusTwo.toString()); // "9007199254740993"
// La igualdad funciona como se espera
console.log(maxSafePlusOne === maxSafePlusTwo); // false
Trabajando con BigInt: Sintaxis y Operaciones
Los BigInt se comportan de manera muy similar a los números normales, pero con algunas diferencias cruciales que cada desarrollador debe entender para evitar errores.
Operaciones Aritméticas
Todos los operadores aritméticos estándar funcionan con BigInts:
- Suma:
+
- Resta:
-
- Multiplicación:
*
- Exponenciación:
**
- Módulo (Resto):
%
El único operador que se comporta de manera diferente es la división (`/`).
const a = 10n;
const b = 3n;
console.log(a + b); // 13n
console.log(a - b); // 7n
console.log(a * b); // 30n
console.log(a ** b); // 1000n
console.log(a % b); // 1n
La Salvedad de la División
Dado que los BigInt solo pueden representar números enteros, el resultado de una división siempre se trunca (la parte fraccionaria se descarta). No redondea.
const a = 10n;
const b = 3n;
console.log(a / b); // 3n (no 3.333...n)
const c = 9n;
const d = 10n;
console.log(c / d); // 0n
Esta es una distinción crítica. Si necesitas realizar cálculos con decimales, BigInt no es la herramienta adecuada. Deberías seguir usando Number
o una biblioteca decimal dedicada.
Comparación e Igualdad
Los operadores de comparación como >
, <
, >=
, y <=
funcionan sin problemas entre BigInts, e incluso entre un BigInt y un Number.
console.log(10n > 5); // true
console.log(10n < 20); // true
console.log(10n > 20n); // false
Sin embargo, la igualdad es más matizada y es una fuente común de confusión.
- Igualdad Débil (`==`): Este operador realiza coerción de tipos. Considera que un BigInt y un Number con el mismo valor matemático son iguales.
- Igualdad Estricta (`===`): Este operador no realiza coerción de tipos. Dado que BigInt y Number son tipos diferentes, siempre devolverá
false
al compararlos.
console.log(10n == 10); // true - ¡Ten cuidado con esto!
console.log(10n === 10); // false - Recomendado para mayor claridad.
console.log(0n == 0); // true
console.log(0n === 0); // false
Mejor Práctica: Para evitar errores sutiles, usa siempre la igualdad estricta (`===`) y sé explícito sobre los tipos que estás comparando. Si necesitas comparar un BigInt y un Number, a menudo es más claro convertir uno al otro primero, teniendo en cuenta la posible pérdida de precisión.
La Discrepancia de Tipos: Una Separación Estricta
JavaScript impone una regla estricta: no puedes mezclar operandos BigInt y Number en la mayoría de las operaciones aritméticas.
Intentar hacerlo resultará en un TypeError
. Esta es una decisión de diseño deliberada para evitar que los desarrolladores pierdan precisión accidentalmente.
const myBigInt = 100n;
const myNumber = 50;
try {
const result = myBigInt + myNumber; // Esto lanzará un error
} catch (error) {
console.log(error); // TypeError: Cannot mix BigInt and other types, use explicit conversions
}
El Enfoque Correcto: Conversión Explícita
Para realizar una operación entre un BigInt y un Number, debes convertir explícitamente uno de ellos.
const myBigInt = 100n;
const myNumber = 50;
// Convertir Number a BigInt (seguro)
const result1 = myBigInt + BigInt(myNumber);
console.log(result1); // 150n
// Convertir BigInt a Number (¡potencialmente inseguro!)
const veryLargeBigInt = 900719925474099199n;
// ¡Esto perderá precisión!
const unsafeNumber = Number(veryLargeBigInt);
console.log(unsafeNumber); // 900719925474099200 - ¡El valor ha sido redondeado!
const safeResult = Number(100n) + myNumber;
console.log(safeResult); // 150
Regla Crítica: Solo convierte un BigInt a un Number si estás absolutamente seguro de que cabe dentro del rango de enteros seguros. De lo contrario, siempre convierte el Number a un BigInt para mantener la precisión.
Casos de Uso Prácticos para BigInt en un Contexto Global
La necesidad de BigInt no es un problema académico abstracto. Resuelve desafíos del mundo real que enfrentan los desarrolladores en varios dominios internacionales.
1. Timestamps de Alta Precisión
El `Date.now()` de JavaScript devuelve el número de milisegundos desde la época Unix. Si bien es suficiente para muchas aplicaciones web, no es lo suficientemente granular para sistemas de alto rendimiento. Muchos sistemas distribuidos, bases de datos y marcos de registro en todo el mundo utilizan marcas de tiempo con precisión de nanosegundos para ordenar eventos con precisión. Estas marcas de tiempo a menudo se representan como enteros de 64 bits, que son demasiado grandes para el tipo Number
.
// Una marca de tiempo de un sistema de alta resolución (por ejemplo, en nanosegundos)
const nanoTimestampStr = "1670000000123456789";
// Usar Number resulta en pérdida de precisión
const lostPrecision = Number(nanoTimestampStr);
console.log(lostPrecision); // 1670000000123456800 - ¡Incorrecto!
// Usar BigInt lo preserva perfectamente
const correctTimestamp = BigInt(nanoTimestampStr);
console.log(correctTimestamp.toString()); // "1670000000123456789"
// Ahora podemos realizar cálculos precisos
const oneSecondInNanos = 1_000_000_000n;
const nextSecond = correctTimestamp + oneSecondInNanos;
console.log(nextSecond.toString()); // "1670001000123456789"
2. Identificadores Únicos (IDs) de APIs
Un escenario muy común es interactuar con APIs que usan enteros de 64 bits para IDs de objetos únicos. Este es un patrón utilizado por grandes plataformas globales como Twitter (IDs de Snowflake) y muchos sistemas de bases de datos (por ejemplo, el tipo `BIGINT` en SQL).
Cuando obtienes datos de una API de este tipo, el analizador JSON en tu navegador o entorno Node.js podría intentar analizar este ID grande como un Number
, lo que lleva a la corrupción de datos antes de que tengas la oportunidad de trabajar con él.
// Una respuesta JSON típica de una API
// Nota: El ID es un número grande, no una cadena.
const jsonResponse = '{"id": 1367874743838343168, "text": "Hello, world!"}';
// Standard JSON.parse corromperá el ID
const parsedData = JSON.parse(jsonResponse);
console.log(parsedData.id); // 1367874743838343200 - ¡ID incorrecto!
// Solución: Asegúrate de que la API envíe IDs grandes como cadenas.
const safeJsonResponse = '{"id": "1367874743838343168", "text": "Hello, world!"}';
const safeParsedData = JSON.parse(safeJsonResponse);
const userId = BigInt(safeParsedData.id);
console.log(userId); // 1367874743838343168n - ¡Correcto!
Esta es la razón por la que es una mejor práctica ampliamente aceptada para las APIs de todo el mundo serializar IDs de enteros grandes como cadenas en cargas útiles JSON para garantizar la compatibilidad con todos los clientes.
3. Criptografía
La criptografía moderna se basa en matemáticas que involucran enteros extremadamente grandes. Algoritmos como RSA dependen de operaciones con números que son de cientos o incluso miles de bits de longitud. BigInt hace posible realizar estos cálculos de forma nativa en JavaScript, lo cual es esencial para aplicaciones criptográficas basadas en web, como las que utilizan la Web Crypto API o implementan protocolos en Node.js.
Si bien un ejemplo criptográfico completo es complejo, podemos ver una demostración conceptual:
// Dos números primos muy grandes (solo para fines de demostración)
const p = 1143400375533529n;
const q = 982451653n; // Uno más pequeño para el ejemplo
// En RSA, los multiplicas para obtener el módulo
const n = p * q;
console.log(n.toString()); // "1123281328905333100311297"
// Este cálculo sería imposible con el tipo Number.
// BigInt lo maneja sin esfuerzo.
4. Aplicaciones Financieras y de Blockchain
Al tratar con finanzas, especialmente en el contexto de las criptomonedas, la precisión es primordial. Muchas criptomonedas, como Bitcoin, miden el valor en su unidad más pequeña (por ejemplo, satoshis). El suministro total de estas unidades puede superar fácilmente a Number.MAX_SAFE_INTEGER
. BigInt es la herramienta perfecta para manejar estas grandes y precisas cantidades sin recurrir a la aritmética de punto flotante, que es propensa a errores de redondeo.
// 1 Bitcoin = 100,000,000 satoshis
const satoshisPerBTC = 100_000_000n;
// El suministro total de Bitcoin es de 21 millones
const totalBTCSupply = 21_000_000n;
// Calcula el total de satoshis
const totalSatoshis = totalBTCSupply * satoshisPerBTC;
// 2,100,000,000,000,000 - Esto es 2.1 cuatrillones
console.log(totalSatoshis.toString());
// Este valor es mayor que Number.MAX_SAFE_INTEGER
console.log(totalSatoshis > BigInt(Number.MAX_SAFE_INTEGER)); // true
Temas Avanzados y Errores Comunes
Serialización y JSON.stringify()
Uno de los problemas más comunes que enfrentan los desarrolladores es la serialización de objetos que contienen BigInts. Por defecto, `JSON.stringify()` no sabe cómo manejar el tipo `bigint` y lanzará un `TypeError`.
const data = {
id: 12345678901234567890n,
user: 'alex'
};
try {
JSON.stringify(data);
} catch (error) {
console.log(error); // TypeError: Do not know how to serialize a BigInt
}
Solución 1: Implementar un método `toJSON`
Puedes indicarle a `JSON.stringify` cómo manejar los BigInts añadiendo un método `toJSON` al `BigInt.prototype`. Este enfoque modifica el prototipo global, lo cual podría no ser deseable en algunos entornos compartidos, pero es muy efectivo.
// Un parche global. Úsalo con consideración.
BigInt.prototype.toJSON = function() {
return this.toString();
};
const data = { id: 12345678901234567890n, user: 'alex' };
const jsonString = JSON.stringify(data);
console.log(jsonString); // '{"id":"12345678901234567890","user":"alex"}'
Solución 2: Usar una función reemplazadora
Un enfoque más seguro y localizado es usar el argumento `replacer` en `JSON.stringify`. Esta función se llama para cada par clave/valor y te permite transformar el valor antes de la serialización.
const data = { id: 12345678901234567890n, user: 'alex' };
const replacer = (key, value) => {
if (typeof value === 'bigint') {
return value.toString();
}
return value;
};
const jsonString = JSON.stringify(data, replacer);
console.log(jsonString); // '{"id":"12345678901234567890","user":"alex"}'
Operaciones a Nivel de Bit
BigInt admite todos los operadores a nivel de bit que te resulten familiares del tipo `Number`: `&` (AND), `|` (OR), `^` (XOR), `~` (NOT), `<<` (desplazamiento a la izquierda) y `>>` (desplazamiento a la derecha con propagación de signo). Estos son especialmente útiles cuando se trabaja con formatos de datos de bajo nivel, permisos o ciertos tipos de algoritmos.
const permissions = 5n; // 0101 en binario
const READ_PERMISSION = 4n; // 0100
const WRITE_PERMISSION = 2n; // 0010
// Comprobar si el permiso de lectura está establecido
console.log((permissions & READ_PERMISSION) > 0n); // true
// Comprobar si el permiso de escritura está establecido
console.log((permissions & WRITE_PERMISSION) > 0n); // false
// Añadir permiso de escritura
const newPermissions = permissions | WRITE_PERMISSION;
console.log(newPermissions); // 7n (que es 0111)
Consideraciones de Rendimiento
Si bien BigInt es increíblemente potente, es importante comprender sus características de rendimiento:
- Number vs. BigInt: Para enteros dentro del rango seguro, las operaciones estándar de
Number
son significativamente más rápidas. Esto se debe a que a menudo pueden mapearse directamente a instrucciones a nivel de máquina procesadas por la CPU de la computadora. Las operaciones de BigInt, al ser de tamaño arbitrario, requieren algoritmos más complejos basados en software. - BigInt vs. Bibliotecas: El `BigInt` nativo es generalmente mucho más rápido que las bibliotecas de números grandes basadas en JavaScript. La implementación es parte del motor de JavaScript (como V8 o SpiderMonkey) y está escrita en un lenguaje de nivel inferior como C++, lo que le da una ventaja de rendimiento significativa.
La Regla de Oro: Usa Number
para todos los cálculos numéricos a menos que tengas una razón específica para creer que los valores podrían exceder Number.MAX_SAFE_INTEGER
. Usa BigInt
cuando necesites sus capacidades, no como un reemplazo predeterminado para todos los números.
Compatibilidad de Navegadores y Entornos
BigInt es una característica moderna de JavaScript, pero su soporte está ahora muy extendido en todo el ecosistema global.
- Navegadores Web: Compatible en todos los principales navegadores modernos (Chrome 67+, Firefox 68+, Safari 14+, Edge 79+).
- Node.js: Compatible desde la versión 10.4.0.
Para proyectos que necesitan admitir entornos muy antiguos, la transpilación con herramientas como Babel puede ser una opción, pero esto conlleva una penalización de rendimiento. Dado el amplio soporte actual, la mayoría de los proyectos nuevos pueden usar BigInt de forma nativa sin preocupaciones.
Conclusión y Mejores Prácticas
BigInt es una adición potente y esencial al lenguaje JavaScript. Proporciona una solución nativa, eficiente y ergonómica al persistente problema de la aritmética de enteros grandes, permitiendo construir una nueva clase de aplicaciones con JavaScript, desde criptografía hasta el manejo de datos de alta precisión.
Para usarlo de manera efectiva y evitar errores comunes, ten en cuenta estas mejores prácticas:
- Usa el Sufijo `n`: Prefiere la sintaxis literal `123n` para crear BigInts. Es claro, conciso y evita la posible pérdida de precisión durante la creación.
- No Mezcles Tipos: Recuerda que no puedes mezclar BigInt y Number en operaciones aritméticas. Sé explícito con tus conversiones: `BigInt()` o `Number()`.
- Prioriza la Precisión: Al convertir entre tipos, siempre prefiere convertir un
Number
a unBigInt
para prevenir la pérdida accidental de precisión. - Usa Igualdad Estricta: Usa `===` en lugar de `==` para comparaciones para evitar comportamientos confusos causados por la coerción de tipos.
- Maneja la Serialización JSON: Planifica la serialización de BigInts. Usa una función `replacer` personalizada en `JSON.stringify` para una solución segura y no global.
- Elige la Herramienta Adecuada: Usa
Number
para matemáticas de propósito general dentro del rango de enteros seguros para un mejor rendimiento. Solo recurre aBigInt
cuando realmente necesites sus capacidades de precisión arbitraria.
Al adoptar BigInt y comprender sus reglas, puedes escribir aplicaciones JavaScript más robustas, precisas y potentes capaces de abordar desafíos numéricos de cualquier escala.