Saltar al contenido principal

deepMerge

Paquete: @tiendana/utils

Fusiona dos objetos de forma recursiva (deep merge), combinando propiedades anidadas y manejando casos especiales como Date, null, undefined y arrays.

Características​

✅ Fusión recursiva de objetos anidados
✅ Preserva instancias de Date
✅ Maneja null y undefined correctamente
✅ No fusiona arrays (sobrescribe)
✅ Type-safe con TypeScript
✅ No modifica target ni source, tampoco los objetos anidados (desde 0.2.10)

Firma​

function deepMerge<T>(
target: T,
source: Partial<T> | null | undefined
): T

Parámetros​

ParámetroTipoDescripciónRequerido
targetTObjeto base que se fusionará✅
sourcePartial<T> | null | undefinedObjeto con propiedades a fusionar en target❌

Retorno​

Tipo: T

Nuevo objeto con las propiedades de target sobrescritas/fusionadas con las de source. Excepciones: si source es undefined retorna el mismo target (misma referencia) y si es null retorna null.

No muta objetos anidados desde 0.2.10

Antes de 0.2.10 la recursión recibía el objeto anidado del target directamente y podía modificarlo. Desde esa versión se fusiona sobre un clon superficial, así que target queda intacto:

const a = { x: { y: 1 } };
const merged = deepMerge(a, { x: { z: 2 } });
// merged: { x: { y: 1, z: 2 } }
// a: { x: { y: 1 } } ← sin cambios

Las propiedades de target que source no toca se copian por referencia, no se clonan en profundidad.

Ejemplos de Uso​

Fusión Básica​

import { deepMerge } from '@tiendana/utils';

const target = {
name: 'John',
age: 30
};

const source = {
age: 31,
city: 'Madrid'
};

const result = deepMerge(target, source);
// {
// name: 'John', ← Preservado de target
// age: 31, ← Sobrescrito por source
// city: 'Madrid' ← Añadido de source
// }

Fusión de Objetos Anidados​

const defaultConfig = {
theme: 'light',
settings: {
notifications: true,
autoSave: false,
language: 'es'
}
};

const userConfig = {
settings: {
autoSave: true
}
};

const config = deepMerge(defaultConfig, userConfig);
// {
// theme: 'light',
// settings: {
// notifications: true, ← Preservado
// autoSave: true, ← Sobrescrito
// language: 'es' ← Preservado
// }
// }

Configuración de Aplicación​

interface AppConfig {
api: {
url: string;
timeout: number;
retries: number;
};
features: {
darkMode: boolean;
analytics: boolean;
};
}

const defaultConfig: AppConfig = {
api: {
url: 'https://api.example.com',
timeout: 5000,
retries: 3
},
features: {
darkMode: false,
analytics: true
}
};

const envConfig: Partial<AppConfig> = {
api: {
url: process.env.API_URL || 'https://api.example.com',
timeout: 10000,
retries: 3
}
};

const finalConfig = deepMerge(defaultConfig, envConfig);
// api.timeout sobrescrito a 10000
// api.url sobrescrito con variable de entorno
// api.retries preservado
// features preservado completamente

Manejo de Fechas​

const target = {
user: 'John',
createdAt: new Date('2024-01-01')
};

const source = {
updatedAt: new Date('2024-12-15')
};

const result = deepMerge(target, source);
// {
// user: 'John',
// createdAt: Date('2024-01-01'), ← Preservado como Date
// updatedAt: Date('2024-12-15') ← Añadido como Date
// }

console.log(result.createdAt instanceof Date); // true

Manejo de Null y Undefined​

const target = {
name: 'John',
age: 30,
city: 'Madrid'
};

const source = {
age: null,
city: undefined
};

const result = deepMerge(target, source);
// {
// name: 'John',
// age: null, ← Sobrescrito a null
// city: undefined ← Sobrescrito a undefined
// }

Source Null o Undefined​

const target = { name: 'John', age: 30 };

deepMerge(target, null);
// null

deepMerge(target, undefined);
// { name: 'John', age: 30 } ← Retorna target sin cambios

Arrays (No Fusiona, Sobrescribe)​

const target = {
tags: ['javascript', 'typescript'],
count: 10
};

const source = {
tags: ['react', 'angular']
};

const result = deepMerge(target, source);
// {
// tags: ['react', 'angular'], ← SOBRESCRITO, no fusionado
// count: 10
// }

Configuración de Componente​

interface ButtonConfig {
style: {
color: string;
background: string;
border: string;
};
behavior: {
disabled: boolean;
loading: boolean;
};
}

const defaultButton: ButtonConfig = {
style: {
color: '#000',
background: '#fff',
border: '1px solid #ccc'
},
behavior: {
disabled: false,
loading: false
}
};

function createButton(custom: Partial<ButtonConfig>): ButtonConfig {
return deepMerge(defaultButton, custom);
}

createButton({
style: {
color: '#fff',
background: '#007bff',
border: 'none'
}
});
// Fusiona solo style, behavior se preserva de defaultButton

Merge de Respuestas de API​

interface User {
id: number;
profile: {
name: string;
email: string;
avatar?: string;
};
settings: {
theme: string;
notifications: boolean;
};
}

function updateUser(current: User, updates: Partial<User>): User {
return deepMerge(current, updates);
}

const currentUser: User = {
id: 1,
profile: {
name: 'John',
email: 'john@example.com'
},
settings: {
theme: 'light',
notifications: true
}
};

const updates = {
profile: {
avatar: 'https://example.com/avatar.jpg',
name: 'John',
email: 'john@example.com'
}
};

const updatedUser = updateUser(currentUser, updates);
// profile.avatar añadido
// settings preservado

Casos de Uso Comunes​

  • Configuración de aplicaciones: Fusionar config por defecto con config de usuario/entorno
  • Temas y estilos: Combinar estilos base con personalizaciones
  • Opciones de componentes: Fusionar props por defecto con props del usuario
  • Estado de aplicación: Actualizar estado preservando propiedades no modificadas
  • API responses: Combinar datos existentes con actualizaciones parciales
  • Settings: Fusionar configuraciones de múltiples fuentes (local, remoto, usuario)

Comportamiento Especial​

Instancias de Date​

Se preservan como instancias de Date, no se fusionan ni se convierten a objetos genéricos.

const result = deepMerge(
{ date: new Date('2024-01-01') },
{ date: new Date('2024-12-15') }
);
result.date instanceof Date; // true

Null vs Undefined​

  • null: Sobrescribe el valor target con null
  • undefined en source: Sobrescribe el valor target con undefined
  • undefined como source: Retorna target sin cambios

Arrays​

Los arrays NO se fusionan, se sobrescriben completamente:

deepMerge(
{ items: [1, 2, 3] },
{ items: [4, 5] }
);
// { items: [4, 5] } ← Sobrescrito, NO [1, 2, 3, 4, 5]

Notas Técnicas​

  • Performance: O(n) donde n es el número de propiedades en ambos objetos
  • Recursión: Usa recursión para objetos anidados
  • Inmutabilidad: Crea un objeto de salida y fusiona los objetos anidados sobre clones superficiales; no modifica target ni source (desde 0.2.10). Lo que source no toca se comparte por referencia con target
  • Type safety: Retorna tipo T del target con propiedades de source fusionadas
  • Shallow copy de primitivos: Primitivos se copian por valor
  • Object detection: Usa typeof === 'object' y !Array.isArray() para detectar objetos fusionables

Limitaciones​

Arrays no se fusionan

Los arrays se sobrescriben completamente, no se fusionan elemento por elemento. Si necesitas fusionar arrays, debes hacerlo manualmente:

const target = { items: [1, 2, 3] };
const source = { items: [4, 5] };

// ❌ Sobrescribe
deepMerge(target, source);
// { items: [4, 5] }

// ✅ Fusiona arrays manualmente
{
...deepMerge(target, source),
items: [...target.items, ...source.items]
}
// { items: [1, 2, 3, 4, 5] }
Clases personalizadas

Las instancias de clases personalizadas (que no sean Date) se tratarán como objetos y se fusionarán propiedad por propiedad, lo que puede causar comportamiento inesperado:

class Person {
constructor(public name: string) {}
greet() { return `Hello, ${this.name}`; }
}

const target = { person: new Person('John') };
const source = { person: new Person('Jane') };

const result = deepMerge(target, source);
result.person.greet(); // ❌ Error: greet no existe

// result.person es un objeto plano, no una instancia de Person

Para preservar instancias de clases, considera agregar verificaciones personalizadas.

Ver También​

  • emptyKeys - Identificar propiedades vacías
  • getPath - Acceder a propiedades anidadas