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ámetro | Tipo | Descripción | Requerido |
|---|---|---|---|
target | T | Objeto base que se fusionará | ✅ |
source | Partial<T> | null | undefined | Objeto 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.
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 connullundefineden source: Sobrescribe el valor target conundefinedundefinedcomo 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
targetnisource(desde 0.2.10). Lo quesourceno toca se comparte por referencia contarget - Type safety: Retorna tipo
Tdel 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
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] }
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.