Volver al blog

Map.getOrInsert ES2026: Finalmente Upsert Nativo en JavaScript

Hola HaWkers, una de las adiciones mas practicas del ES2026 finalmente resuelve un problema que todo desarrollador JavaScript ha enfrentado: verificar si una clave existe en el Map antes de insertar. Los nuevos metodos getOrInsert y getOrInsertComputed llegan a Chrome 145 en Enero 2026.

Basta de if (!map.has(key)) { map.set(key, value); }. Vamos a ver como usarlo.

El Problema Actual

Por que necesitamos upsert.

Patron Repetitivo

Codigo que todo el mundo escribe:

// ANTES: Agrupando items por categoria
const groups = new Map();

for (const item of items) {
  if (!groups.has(item.category)) {
    groups.set(item.category, []);
  }
  groups.get(item.category).push(item);
}

// O la version "inteligente" pero confusa:
for (const item of items) {
  const arr = groups.get(item.category) ?? [];
  if (!groups.has(item.category)) {
    groups.set(item.category, arr);
  }
  arr.push(item);
}

Problemas de Este Enfoque

Por que es malo:

// 1. DOS BUSQUEDAS en la misma clave
if (!map.has(key)) {  // <- busqueda 1
  map.set(key, []);
}
map.get(key);         // <- busqueda 2

// 2. MUY VERBOSE
// 3 lineas para algo que deberia ser 1

// 3. PROPENSO A ERRORES
// Facil olvidar verificar antes
const value = map.get(key);
value.push(item);  // TypeError si la clave no existe!

// 4. NO ATOMICO
// En teoria, otra operacion podria interferir
// entre has() y set() (menos relevante en JS single-thread)

La Solucion: getOrInsert

Nuevo metodo nativo.

Sintaxis

Como funciona:

// getOrInsert(key, defaultValue)
// Retorna valor existente O inserta y retorna defaultValue

const map = new Map();

// Si 'a' no existe, inserta 0 y retorna 0
const value = map.getOrInsert('a', 0);
console.log(value);  // 0
console.log(map.get('a'));  // 0

// Si 'a' ya existe, retorna valor existente
map.set('a', 42);
const existing = map.getOrInsert('a', 0);
console.log(existing);  // 42 (no sobrescribe)

Ejemplo Practico: Contador

Contando ocurrencias:

// ANTES (verbose):
const counts = new Map();
for (const word of words) {
  if (!counts.has(word)) {
    counts.set(word, 0);
  }
  counts.set(word, counts.get(word) + 1);
}

// DESPUES (limpio):
const counts = new Map();
for (const word of words) {
  const count = counts.getOrInsert(word, 0);
  counts.set(word, count + 1);
}

// O aun mas limpio con objeto mutable:
const counts = new Map();
for (const word of words) {
  const counter = counts.getOrInsert(word, { count: 0 });
  counter.count++;
}

Ejemplo Practico: Agrupamiento

Agrupando por propiedad:

// ANTES:
function groupBy(items, keyFn) {
  const groups = new Map();
  for (const item of items) {
    const key = keyFn(item);
    if (!groups.has(key)) {
      groups.set(key, []);
    }
    groups.get(key).push(item);
  }
  return groups;
}

// DESPUES:
function groupBy(items, keyFn) {
  const groups = new Map();
  for (const item of items) {
    const key = keyFn(item);
    groups.getOrInsert(key, []).push(item);
  }
  return groups;
}

// Uso:
const byCategory = groupBy(products, p => p.category);

getOrInsertComputed

Para valores dinamicos.

Por Que Existe

El problema con getOrInsert:

// getOrInsert siempre crea el valor default
// aunque no lo necesite

class ExpensiveObject {
  constructor() {
    console.log('Creando objeto costoso...');
    // Operacion pesada
  }
}

const cache = new Map();

// PROBLEMA: ExpensiveObject se crea SIEMPRE
// aunque 'key' ya exista en el map!
cache.getOrInsert('key', new ExpensiveObject());

// Cada llamada crea un nuevo objeto,
// aunque el cache ya tenga el valor

Sintaxis

Como funciona:

// getOrInsertComputed(key, callbackFn)
// callbackFn solo se llama si key NO existe

const cache = new Map();

// Callback solo ejecuta si 'key' no existe
const value = cache.getOrInsertComputed('key', () => {
  console.log('Computando valor...');
  return new ExpensiveObject();
});

// Segunda llamada: callback NO ejecuta
const cached = cache.getOrInsertComputed('key', () => {
  console.log('Esto no va a aparecer');
  return new ExpensiveObject();
});

console.log(value === cached);  // true

Ejemplo Practico: Cache de Computacion

Memoization:

// Cache de resultados computados
const computationCache = new Map();

function computeExpensive(input) {
  return computationCache.getOrInsertComputed(input, () => {
    console.log(`Computando para: ${input}`);
    // Operacion pesada
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i * input);
    }
    return result;
  });
}

// Primera llamada: computa
computeExpensive(42);  // "Computando para: 42"

// Segunda llamada: cache hit
computeExpensive(42);  // (silencioso, retorna cache)

// Nuevo input: computa de nuevo
computeExpensive(100);  // "Computando para: 100"

Ejemplo Practico: Factory Pattern

Creacion perezosa:

class ConnectionPool {
  #connections = new Map();

  getConnection(database) {
    return this.#connections.getOrInsertComputed(database, () => {
      console.log(`Creando conexion para: ${database}`);
      return new DatabaseConnection(database);
    });
  }
}

const pool = new ConnectionPool();

// Primera llamada: crea conexion
const conn1 = pool.getConnection('users');

// Misma conexion reutilizada
const conn2 = pool.getConnection('users');
console.log(conn1 === conn2);  // true

// Nueva conexion para otro banco
const conn3 = pool.getConnection('products');
console.log(conn1 === conn3);  // false

WeakMap Tambien Gana

Mismos metodos.

Soporte en WeakMap

Funciona igual:

const weakCache = new WeakMap();

class Component {
  constructor(id) {
    this.id = id;
  }
}

const component = new Component(1);

// getOrInsert en WeakMap
const metadata = weakCache.getOrInsert(component, {
  renderCount: 0,
  lastUpdate: null,
});

metadata.renderCount++;

// getOrInsertComputed en WeakMap
const computed = weakCache.getOrInsertComputed(component, () => ({
  expensiveData: calculateExpensiveData(component),
}));

Caso de Uso: Metadatos de Objetos

Asociando datos a objetos:

// Metadatos sin leak de memoria
const objectMetadata = new WeakMap();

function trackObject(obj) {
  const meta = objectMetadata.getOrInsert(obj, {
    createdAt: Date.now(),
    accessCount: 0,
    history: [],
  });
  meta.accessCount++;
  meta.history.push(Date.now());
  return obj;
}

// Cuando obj es garbage collected,
// metadatos tambien se limpian

Comparando Con Alternativas

Por que usar el nativo.

Object.groupBy

Comparacion:

// Object.groupBy (ES2024) - retorna objeto
const grouped = Object.groupBy(items, item => item.category);
// { electronics: [...], clothing: [...] }

// Map.getOrInsert - retorna Map
const grouped = new Map();
for (const item of items) {
  grouped.getOrInsert(item.category, []).push(item);
}

// Cuando usar cada uno:
// - Object.groupBy: resultado final es objeto, claves son strings
// - Map.getOrInsert: necesita Map, claves pueden ser cualquier tipo

Lodash _.get / _.set

Comparacion:

// Lodash
import _ from 'lodash';

const obj = {};
_.set(obj, 'a.b.c', []);
_.get(obj, 'a.b.c', []).push(item);

// Nativo con Map
const map = new Map();
map.getOrInsert('key', []).push(item);

// Ventajas del nativo:
// - Sin dependencia
// - Mejor performance
// - Tipos correctos (TypeScript)

Polyfill Simple

Para browsers antiguos:

// Polyfill para Map.prototype.getOrInsert
if (!Map.prototype.getOrInsert) {
  Map.prototype.getOrInsert = function(key, defaultValue) {
    if (!this.has(key)) {
      this.set(key, defaultValue);
    }
    return this.get(key);
  };
}

// Polyfill para Map.prototype.getOrInsertComputed
if (!Map.prototype.getOrInsertComputed) {
  Map.prototype.getOrInsertComputed = function(key, callbackFn) {
    if (!this.has(key)) {
      this.set(key, callbackFn(key));
    }
    return this.get(key);
  };
}

Performance

Por que el nativo es mejor.

Benchmark

Comparando enfoques:

Operacion: 1M upserts

Patron has() + set() + get():
├── Tiempo: 245ms
├── Operaciones: 3 por upsert
└── Overhead: busqueda duplicada

getOrInsert nativo:
├── Tiempo: 89ms
├── Operaciones: 1 por upsert
└── Overhead: minimo

Mejora: ~2.7x mas rapido

Por Que Es Mas Rapido

Optimizaciones internas:

// Patron antiguo - 3 operaciones:
if (!map.has(key)) {    // 1. Hash + busqueda
  map.set(key, value);  // 2. Hash + busqueda + insercion
}
return map.get(key);    // 3. Hash + busqueda

// getOrInsert - 1 operacion:
map.getOrInsert(key, value);  // 1. Hash + busqueda + condicional
// Internamente optimizado por el engine

Patrones Comunes

Refactorizando codigo existente.

Contador de Frecuencia

Antes y despues:

// ANTES:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    freq.set(item, (freq.get(item) || 0) + 1);
  }
  return freq;
}

// DESPUES:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    const counter = freq.getOrInsert(item, { n: 0 });
    counter.n++;
  }
  return freq;
}

// O con valor primitivo:
function countFrequency(items) {
  const freq = new Map();
  for (const item of items) {
    const count = freq.getOrInsert(item, 0);
    freq.set(item, count + 1);
  }
  return freq;
}

Adjacency List (Grafos)

Antes y despues:

// ANTES:
function buildGraph(edges) {
  const graph = new Map();
  for (const [from, to] of edges) {
    if (!graph.has(from)) graph.set(from, []);
    if (!graph.has(to)) graph.set(to, []);
    graph.get(from).push(to);
  }
  return graph;
}

// DESPUES:
function buildGraph(edges) {
  const graph = new Map();
  for (const [from, to] of edges) {
    graph.getOrInsert(from, []).push(to);
    graph.getOrInsert(to, []);  // Garantiza que el nodo existe
  }
  return graph;
}

Multi-Map

Mapeando para multiples valores:

// ANTES:
class MultiMap {
  #map = new Map();

  add(key, value) {
    if (!this.#map.has(key)) {
      this.#map.set(key, new Set());
    }
    this.#map.get(key).add(value);
  }
}

// DESPUES:
class MultiMap {
  #map = new Map();

  add(key, value) {
    this.#map.getOrInsert(key, new Set()).add(value);
  }
}

Timeline de Soporte

Cuando usar en produccion.

Status Actual (Enero 2026)

Implementacion:

Browser Status Version
Chrome Estable 145+
Edge Estable 145+
Firefox En desarrollo ~130
Safari En desarrollo ~27
Node.js Estable 22+
Deno Estable 1.40+
Bun Estable 1.1+

Recomendacion

Cuando adoptar:

Ahora (Enero 2026):
├── Node.js / Deno / Bun: usar libremente
├── Browser moderno: Chrome/Edge ok
├── Todos browsers: usar con polyfill
└── TypeScript: tipos ya disponibles

Q2 2026 (previsto):
├── Firefox estable
├── Safari estable
└── Remover polyfill para mayoria de usuarios

Q4 2026 (previsto):
├── Baseline feature
└── Uso sin preocupacion

Conclusion

getOrInsert y getOrInsertComputed son el tipo de adicion que parece pequena pero impacta codigo real diariamente. El patron "verificar si existe, insertar si no, obtener valor" es tan comun que tener soporte nativo elimina decenas de lineas repetitivas por proyecto.

La performance tambien importa: evitar busquedas duplicadas en el Map puede hacer diferencia en loops intensos. Y la semantica es mas clara - el nombre del metodo dice exactamente lo que hace.

Para proyectos nuevos en Node.js o browsers modernos, empieza a usar ahora. Para proyectos que necesitan compatibilidad amplia, el polyfill de 10 lineas resuelve hasta que todos los browsers implementen.

Es otro paso de JavaScript hacia la ergonomia que otros lenguajes tienen hace anos.

Si quieres entender mas sobre las nuevas features de ES2026, consulta nuestro articulo sobre Import Defer para otra adicion importante de la version.

Vamos con todo! 🦅

💻 Domina JavaScript de Verdad

El conocimiento que adquiriste en este articulo es solo el comienzo. Entender Map y estructuras de datos es fundamental para codigo eficiente.

Invierte en Tu Futuro

Prepare material completo para que domines JavaScript:

Formas de pago:

  • 1x de $4.90 sin intereses
  • o $4.90 al contado

📖 Ver Contenido Completo

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios