Quiero una función que dados varios objetos que no tienen ninguna key en común, genere un único objeto con todas las propiedades de cada objeto. Quiero que sea en Typescript con el mayor tipado posible.
Perfecto—esto es básicamente un merge de objetos disjuntos con inferencia fuerte de tipos.
La clave es lograr que el resultado sea la intersección de todos los tipos de entrada.
🧠 Objetivo
merge(
{ a: 1 },
{ b: "x" },
{ c: true }
)
👉 resultado (y tipo):
{ a: number; b: string; c: boolean }
✅ Implementación con tipado fuerte
// Convierte una unión en intersección
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends
(k: infer I) => void
? I
: never;
// Helper para mejorar legibilidad del tipo final
type Simplify<T> = { [K in keyof T]: T[K] };
export function mergeObjects<
T extends readonly Record<PropertyKey, unknown>[]
>(
...objs: T
): Simplify<UnionToIntersection<T[number]>> {
return Object.assign({}, ...objs);
}
🔥 Uso
const result = mergeObjects(
{ a: 1 },
{ b: "hola" },
{ c: true }
);
👉 Tipo inferido:
{
a: number;
b: string;
c: boolean;
}
🧠 Por qué funciona
1. T[number]
T = [{a}, {b}, {c}]
T[number] = {a} | {b} | {c}
2. UnionToIntersection
{a} | {b} | {c}
→ {a} & {b} & {c}
3. Resultado final
{ a } & { b } & { c }
→ { a, b, c }
⚠️ Suposición importante
Esto funciona perfecto porque vos garantizás:
❗ “no tienen ninguna key en común”
Si hubiera conflicto:
{ a: number } & { a: string }
👉 a → never 💥
🚀 Bonus: hacer que falle si hay keys duplicadas
Si querés máxima seguridad, se puede agregar validación de tipos para evitar colisiones (más avanzado, pero posible).
🧠 Alternativa simple (menos estricta)
function mergeLoose(...objs: object[]) {
return Object.assign({}, ...objs);
}
👉 funciona, pero:
- ❌ pierde inferencia precisa
-
❌ tipo:
object
🧩 TL;DR
✔ Usás:
-
T[number]→ unión de tipos -
UnionToIntersection→ intersección -
Object.assign→ runtime
👉 Resultado:
tipo exacto con todas las propiedades combinadas
Si querés, en el siguiente paso puedo darte:
👉 versión que detecta conflictos de keys en compile-time
👉 o una que hace merge profundo (deep merge) con tipado fuerte (esto ya es nivel avanzado heavy 😄)
Buenísimo—esto es un clásico de “reverse lookup” tipado.
La clave es que el tipo de retorno sea solo las keys cuyo valor coincide con el tipo del valor que pasás.
🧠 Objetivo
const obj = {
a: 1,
b: "x",
c: 1,
d: true
};
keysByValue(obj, 1);
// → ["a", "c"]
// tipo: ("a" | "c")[]
✅ Implementación bien tipada
// Extrae las keys de T cuyos valores son asignables a V
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T];
export function keysByValue<
T extends Record<PropertyKey, unknown>,
V
>(
obj: T,
value: V
): KeysMatching<T, V>[] {
const result: KeysMatching<T, V>[] = [];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const k = key as keyof T;
// comparación en runtime
if (obj[k] === value) {
result.push(k as KeysMatching<T, V>);
}
}
}
return result;
}
🔥 Uso con inferencia fuerte
const obj = {
a: 1,
b: "x",
c: 1,
d: true
} as const;
const r = keysByValue(obj, 1);
👉 tipo de r:
("a" | "c")[]
⚠️ IMPORTANTE (muy importante para el tipado)
Para que funcione bien necesitás:
as const
👉 si no:
const obj = {
a: 1,
b: "x"
};
TypeScript infiere:
{ a: number; b: string }
Entonces:
keysByValue(obj, 1)
👉 tipo:
("a" | "b")[]
💥 porque number podría coincidir con más cosas
🧠 Por qué funciona
1. Mapped type
[K in keyof T]: T[K] extends V ? K : never
👉 filtra keys
2. Indexación final
[keyof T]
👉 convierte en unión:
"a" | "c"
🚀 Variante más estricta (igualdad exacta)
Si querés evitar matches amplios (number vs 1):
type KeysExactlyMatching<T, V> = {
[K in keyof T]: [T[K]] extends [V]
? [V] extends [T[K]]
? K
: never
: never
}[keyof T];
🧩 TL;DR
✔ devuelve las keys cuyo valor coincide
✔ tipado fuerte usando KeysMatching
✔ mejor precisión con as const
Si querés, puedo darte una versión:
👉 que soporte comparación custom (deepEqual, por ejemplo)
👉 o que devuelva directamente un Record filtrado en lugar de un array (muy útil en pipelines)
Perfecto—vamos a hacerlo con comparación custom (ej: deepEqual) y manteniendo el mejor tipado posible.
🧠 Punto clave (importante)
Cuando usás un comparador custom:
❗ TypeScript ya no puede inferir qué keys matchean en base al valor
👉 porque la lógica vive en runtime
💡 Solución elegante
Hacemos dos cosas:
- Permitimos un comparador custom
- Permitimos opcionalmente un type guard para recuperar tipado fuerte
✅ Implementación
// ===== Tipos =====
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never
}[keyof T];
// Comparador genérico
type Comparator<T, V> = (a: T, b: V) => boolean;
// Type guard opcional
type Guard<T, V> = (a: T, b: V) => a is Extract<T, V>;
// ===== Función =====
export function keysByValue<
T extends Record<PropertyKey, unknown>,
V
>(
obj: T,
value: V,
comparator: Comparator<T[keyof T], V>,
guard?: Guard<T[keyof T], V>
): (keyof T)[] {
const result: (keyof T)[] = [];
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const k = key as keyof T;
if (comparator(obj[k], value)) {
result.push(k);
}
}
}
return result;
}
🔍 Ejemplo con deepEqual
Primero un deepEqual simple:
function deepEqual(a: unknown, b: unknown): boolean {
return JSON.stringify(a) === JSON.stringify(b);
}
Uso
const obj = {
a: { x: 1 },
b: { x: 2 },
c: { x: 1 }
} as const;
const keys = keysByValue(obj, { x: 1 }, deepEqual);
👉 resultado:
["a", "c"]
👉 tipo:
(keyof typeof obj)[]
🔥 Versión con type guard (más tipado)
Si querés recuperar precisión:
function deepEqualGuard<T>(
a: unknown,
b: T
): a is T {
return JSON.stringify(a) === JSON.stringify(b);
}
Uso:
const keys = keysByValue(
obj,
{ x: 1 },
deepEqual,
deepEqualGuard
);
🧠 ¿Por qué no puede tipar perfecto?
Porque esto:
deepEqual(obj[k], value)
👉 es una caja negra para TypeScript
No sabe:
- qué valores coinciden
- ni qué keys corresponden
🚀 Variante alternativa (más poderosa)
Podés invertir el control:
export function keysByPredicate<
T extends Record<PropertyKey, unknown>,
K extends keyof T
>(
obj: T,
predicate: (value: T[K], key: K) => boolean
): K[] {
const result: K[] = [];
for (const key in obj) {
const k = key as K;
if (predicate(obj[k], k)) {
result.push(k);
}
}
return result;
}
Uso:
keysByPredicate(obj, (v) => deepEqual(v, { x: 1 }));
🧩 TL;DR
- ✔ soportar
deepEquales fácil en runtime - ❗ pero se pierde precisión de tipos automáticamente
- ✔ podés recuperar algo con type guards
- ✔ alternativa: usar predicate (más flexible)
Si querés, puedo darte una versión:
👉 con deepEqual robusto (sin JSON.stringify)
👉 o una que además devuelva { key, value }[] tipado (muy útil en pipelines funcionales)
Ahh, perfecto—esto es uno de los trucos más profundos de TypeScript 😄
Te lo explico paso a paso y sin magia.
🧠 El código
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends
(k: infer I) => void
? I
: never;
🎯 Objetivo
Convertir esto:
A | B | C
👉 en:
A & B & C
🧠 Paso 1: Distribución sobre uniones
U extends any ? (k: U) => void : never
👉 Esto es un conditional type distributivo
Si:
U = A | B | C
👉 se transforma en:
(k: A) => void | (k: B) => void | (k: C) => void
💥 ahora tenés una unión de funciones
🧠 Paso 2: Inferencia desde funciones
Ahora viene esto:
(... ) extends (k: infer I) => void
👉 TypeScript intenta hacer match con:
(k: infer I) => void
🔥 Paso clave (EL TRUCO)
Cuando TypeScript infiere desde una unión de funciones:
(k: A) => void | (k: B) => void
👉 el parámetro k se infiere como:
A & B
🤯 ¿Por qué pasa esto?
Porque los parámetros de funciones son contravariantes.
👉 Simplificado:
- retorno → unión (
|) - parámetros → intersección (
&)
🧠 Intuición mental
Si una función debe aceptar:
(k: A) => void
y también:
(k: B) => void
👉 entonces debe aceptar algo que sea:
A & B
💥 porque tiene que servir para ambos casos
🧪 Ejemplo concreto
type U = { a: number } | { b: string };
type F =
U extends any ? (k: U) => void : never;
👉 se vuelve:
(k: { a: number }) => void |
(k: { b: string }) => void
Ahora inferimos:
F extends (k: infer I) => void ? I : never;
👉 resultado:
{ a: number } & { b: string }
🔥 Resumen del flujo
- Unión:
A | B
- Distribución:
(k: A) => void | (k: B) => void
- Inferencia:
(k: infer I) => void
- Resultado:
A & B
🧠 TL;DR (clave mental)
TypeScript convierte unión → intersección usando:
👉 distribución + contravarianza de funciones
⚠️ Nota importante
Esto funciona porque:
- los parámetros de funciones son contravariantes
- TypeScript usa esa propiedad en la inferencia
🚀 Insight final
Este patrón es la base de:
UnionToIntersection- inferencias avanzadas
- utilidades tipo
Merge,PropsOf, etc.
Si querés, en el siguiente paso puedo:
👉 mostrarte una visualización aún más intuitiva (tipo “diagrama mental”)
👉 o darte ejemplos donde esto falla o se vuelve contraintuitivo (también pasa 😄)