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 😄)
Buen caso 👌—básicamente querés un “filter que preserva la forma del array” (mantiene índices), dejando “huecos” donde no matchea.
🧠 Idea
- Mismo largo que el array original
- Si cumple → queda el valor
-
Si no → queda un slot vacío (hole) o
undefined(según prefieras)
✅ Opción 1 (recomendada): usar undefined (mejor para TS)
export function filterKeepShape<T>(
arr: readonly T[],
predicate: (value: T, index: number, array: readonly T[]) => boolean
): (T | undefined)[] {
const result: (T | undefined)[] = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i, arr)) {
result[i] = arr[i];
} else {
result[i] = undefined;
}
}
return result;
}
🔥 Uso
const arr = [1, 2, 3, 4];
const r = filterKeepShape(arr, x => x % 2 === 0);
// resultado:
[undefined, 2, undefined, 4]
👉 tipo:
(number | undefined)[]
🧠 Opción 2: con type predicate (como filter real)
export function filterKeepShapeTyped<T, S extends T>(
arr: readonly T[],
predicate: (value: T, index: number, array: readonly T[]) => value is S
): (S | undefined)[] {
const result: (S | undefined)[] = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i, arr)) {
result[i] = arr[i];
} else {
result[i] = undefined;
}
}
return result;
}
🔥 Uso con type guard
const arr: (number | string)[] = [1, "a", 2];
const isNumber = (x: number | string): x is number =>
typeof x === "number";
const r = filterKeepShapeTyped(arr, isNumber);
// tipo:
(number | undefined)[]
⚠️ Opción 3: “slots vacíos reales” (holes)
export function filterKeepHoles<T>(
arr: readonly T[],
predicate: (value: T, index: number, array: readonly T[]) => boolean
): T[] {
const result = new Array<T>(arr.length);
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i, arr)) {
result[i] = arr[i];
}
// si no → queda hole
}
return result;
}
🔥 Resultado
[ , 2, , 4 ] // holes reales
⚠️ IMPORTANTE (diferencia clave)
| Variante | Resultado | Tipado | Comportamiento |
|---|---|---|---|
undefined | [undefined, 2, ...] | fuerte ✅ | predecible |
| holes | [ , 2, , ...] | débil ⚠️ | JS raro |
👉 Ejemplo:
[ , 2 ].map(x => x) // → [ , 2 ] (no ejecuta en holes)
🧠 Recomendación
👉 Usá
undefined, no holes
porque:
- más predecible
- mejor tipado
- menos bugs sutiles
🧩 TL;DR
✔ sí, se puede
✔ versión recomendada:
(T | undefined)[]
✔ soporta type guards como filter
Si querés, puedo darte una versión más avanzada:
👉 que preserve exactamente el tuple type (nivel TS muy alto 😄)
Buenísimo—esto combina varias cosas: mapear un objeto, aplicar tu filterKeepShape y luego eliminar keys cuyo array queda todo undefined.
Te dejo una solución tipada, razonablemente estricta y práctica.
🧱 Reutilizamos filterKeepShape
export function filterKeepShape<T>(
arr: readonly T[],
predicate: (value: T, index: number, array: readonly T[]) => boolean
): (T | undefined)[] {
const result: (T | undefined)[] = new Array(arr.length);
for (let i = 0; i < arr.length; i++) {
result[i] = predicate(arr[i], i, arr) ? arr[i] : undefined;
}
return result;
}
🧠 Tipo del objeto de entrada
type ArrayRecord = Record<PropertyKey, readonly unknown[]>;
✅ Función principal
export function filterObjectArrays<
T extends ArrayRecord
>(
obj: T,
predicate: (
value: T[keyof T][number],
key: keyof T,
index: number
) => boolean
): Partial<{
[K in keyof T]: (T[K][number] | undefined)[];
}> {
const result: Partial<Record<keyof T, unknown[]>> = {};
for (const key in obj) {
const arr = obj[key];
const filtered = filterKeepShape(arr, (v, i) =>
predicate(v as T[keyof T][number], key, i)
);
// eliminar keys cuyo array queda todo undefined
const hasAnyValue = filtered.some(v => v !== undefined);
if (hasAnyValue) {
result[key] = filtered;
}
}
return result as Partial<{
[K in keyof T]: (T[K][number] | undefined)[];
}>;
}
🔥 Uso
const data = {
a: [1, 2, 3],
b: [4, 5],
c: [6]
} as const;
const r = filterObjectArrays(
data,
(v) => v % 2 === 0
);
🔍 Resultado
{
a: [undefined, 2, undefined],
b: [4, undefined]
// c desaparece
}
🧠 Tipado resultante
Partial<{
a: (number | undefined)[];
b: (number | undefined)[];
c: (number | undefined)[];
}>
👉 Partial porque:
- algunas keys pueden desaparecer
⚠️ Detalles importantes
1. El predicado es “global”
(value: T[keyof T][number], key, index)
👉 puede operar sobre arrays de distintos tipos
👉 pero pierde precisión por key (trade-off razonable)
2. Narrowing por key (opcional más avanzado)
Si quisieras ultra precisión por key:
👉 la firma se vuelve mucho más compleja (mapped generics por key)
3. Runtime vs tipos
TypeScript no puede saber en compile-time:
- qué arrays quedarán vacíos
👉 por eso usamos Partial
🚀 Variante con type guard
Si querés soportar predicates tipo value is S:
export function filterObjectArraysTyped<
T extends ArrayRecord,
S
>(
obj: T,
predicate: (
value: T[keyof T][number],
key: keyof T,
index: number
) => value is S
): Partial<{
[K in keyof T]: (S | undefined)[];
}> {
const result: Partial<Record<keyof T, unknown[]>> = {};
for (const key in obj) {
const arr = obj[key];
const filtered = filterKeepShape(arr, (v, i) =>
predicate(v as T[keyof T][number], key, i)
);
if (filtered.some(v => v !== undefined)) {
result[key] = filtered;
}
}
return result as Partial<{
[K in keyof T]: (S | undefined)[];
}>;
}
🧩 TL;DR
✔ aplica filterKeepShape a cada array
✔ elimina keys sin valores útiles
✔ mantiene tipado razonable con Partial
✔ soporta predicates complejos
Si querés, el siguiente nivel sería:
👉 preservar tipos por key individualmente (full precision)
👉 o mantener tuplas exactas en vez de arrays (eso ya es nivel TS hardcore 😄)
Buenísimo—esto es el mismo patrón que hiciste con arrays, pero ahora sobre valores escalares del objeto.
Te doy dos variantes claras:
-
filterObjectValuesTyped→ filtra por valor -
filterObjectKeysTyped→ filtra por key
Ambas con soporte para type predicate (value is S).
🧠 Base
type AnyRecord = Record<PropertyKey, unknown>;
✅ 1. filterObjectValuesTyped (tipo guard sobre valores)
type ValuesMatching<T, S> = {
[K in keyof T]: T[K] extends S ? K : never
}[keyof T];
export function filterObjectValuesTyped<
T extends AnyRecord,
S
>(
obj: T,
predicate: (value: T[keyof T], key: keyof T) => value is S
): Partial<Pick<T, ValuesMatching<T, S>>> {
const result: Partial<Record<keyof T, unknown>> = {};
for (const key in obj) {
const k = key as keyof T;
const value = obj[k];
if (predicate(value, k)) {
result[k] = value;
}
}
return result as Partial<Pick<T, ValuesMatching<T, S>>>;
}
🔥 Uso
const obj = {
a: 1,
b: "hola",
c: 2,
d: true
} as const;
const isNumber = (v: unknown): v is number =>
typeof v === "number";
const r = filterObjectValuesTyped(obj, isNumber);
👉 tipo:
Partial<{
a: number;
c: number;
}>
✅ 2. filterObjectKeysTyped (filtrado por key)
export function filterObjectKeysTyped<
T extends AnyRecord,
K extends keyof T
>(
obj: T,
predicate: (key: keyof T) => key is K
): Partial<Pick<T, K>> {
const result: Partial<Record<keyof T, unknown>> = {};
for (const key in obj) {
const k = key as keyof T;
if (predicate(k)) {
result[k] = obj[k];
}
}
return result as Partial<Pick<T, K>>;
}
🔥 Uso
const obj = {
a: 1,
b: "hola",
c: 2
};
const r = filterObjectKeysTyped(
obj,
(k): k is "a" | "c" => k === "a" || k === "c"
);
👉 tipo:
Partial<{
a: number;
c: number;
}>
🧠 Alternativa más flexible (sin type guard)
Si no querés type guard:
export function filterObjectValues<
T extends AnyRecord
>(
obj: T,
predicate: (value: T[keyof T], key: keyof T) => boolean
): Partial<T> {
const result: Partial<T> = {};
for (const key in obj) {
const k = key as keyof T;
if (predicate(obj[k], k)) {
result[k] = obj[k];
}
}
return result;
}
⚠️ Por qué usamos Partial
Porque:
❗ en runtime no sabemos qué keys van a sobrevivir
🔥 Insight importante
Esto:
Pick<T, ...>
Partial<...>
👉 modela exactamente:
“subset dinámico de propiedades”
🧩 TL;DR
-
✔
filterObjectValuesTyped→ filtra por valor con type guard -
✔
filterObjectKeysTyped→ filtra por key con type guard -
✔ resultado:
Partial<Pick<...>> - ✔ buen balance entre precisión y practicidad
Si querés, puedo llevar esto más lejos:
👉 hacer una versión que no use Partial cuando el predicate es determinístico en compile-time
👉 o una que combine key + value narrowing simultáneo (nivel TS bastante pro)
Buenísimo, ese error es 100% TypeScript siendo estricto con los type predicates, y tiene sentido.
🧠 El problema
Tu firma probablemente es algo así:
predicate: (value: T[keyof T], key: keyof T) => value is S
Y TS te dice:
❌ “
Sno necesariamente es compatible conT[keyof T]”
👉 Porque podrías hacer algo ilegal como:
filterObjectValuesTyped(obj, (v): v is Date => true);
aunque obj tenga solo string | number.
✅ Solución correcta
Tenés que restringir S:
S extends T[keyof T]
🔧 Versión corregida
type AnyRecord = Record<PropertyKey, unknown>;
type ValuesMatching<T, S> = {
[K in keyof T]: T[K] extends S ? K : never
}[keyof T];
export function filterObjectValuesTyped<
T extends AnyRecord,
S extends T[keyof T]
>(
obj: T,
predicate: (value: T[keyof T], key: keyof T) => value is S
): Partial<Pick<T, ValuesMatching<T, S>>> {
const result: Partial<Record<keyof T, unknown>> = {};
for (const key in obj) {
const k = key as keyof T;
const value = obj[k];
if (predicate(value, k)) {
result[k] = value;
}
}
return result as Partial<Pick<T, ValuesMatching<T, S>>>;
}
🧠 Por qué esto lo arregla
Antes:
S // podía ser cualquier cosa
Ahora:
S extends T[keyof T]
👉 o sea:
“S es un subtipo de los valores posibles del objeto”
✔ TypeScript queda conforme
✔ el type predicate es válido
🔥 Insight importante
Este error viene de una regla clave:
Un type predicate
value is Ssolo es válido siSes compatible con el tipo original devalue
🧪 Ejemplo válido
const obj = {
a: 1,
b: "hola"
};
const isNumber = (v: unknown): v is number =>
typeof v === "number";
filterObjectValuesTyped(obj, isNumber); // ✅
❌ Ejemplo inválido (y ahora correctamente rechazado)
const isDate = (v: unknown): v is Date => true;
filterObjectValuesTyped(obj, isDate);
// ❌ ahora TypeScript lo bloquea
🧩 TL;DR
✔ El error aparece porque S era demasiado libre
✔ Se soluciona con:
S extends T[keyof T]
✔ Esto hace que el type predicate sea válido
Si querés, puedo mostrarte una versión todavía más avanzada donde:
👉 el narrowing se hace por key individual (no global)
👉 y el resultado queda aún más preciso (nivel TS bastante fino)
Excelente observación—esto es una sutileza profunda de TypeScript 👀
y la diferencia no es obvia a primera vista.
🧠 TL;DR corto
En
filterObjectArraysTypedno da error porque no estás usandoSpara restringirT
EnfilterObjectValuesTypedsí lo hacías indirectamente → por eso TypeScript exige la restricción
🔍 Vamos paso a paso
🟢 Caso que NO rompe (filterObjectArraysTyped)
predicate: (value: T[keyof T][number]) => value is S
// retorno:
(S | undefined)[]
👉 clave:
-
Sno se usa para indexar T -
no se usa en
Pick, ni enkeyof - no afecta la estructura del objeto original
👉 solo estás diciendo:
“si pasa el predicate → lo trato como S”
💡 TypeScript lo permite porque es local, no estructural.
🔴 Caso que SÍ rompe (filterObjectValuesTyped)
Partial<Pick<T, ValuesMatching<T, S>>>
y:
type ValuesMatching<T, S> = {
[K in keyof T]: T[K] extends S ? K : never
}[keyof T];
👉 acá pasa algo MUY importante:
❗ estás usando
Spara decidir qué keys existen en el tipo resultante
💥 El problema real
Si S fuera libre:
S = Date
y:
T = { a: number; b: string }
Entonces:
T[K] extends S // nunca
👉 resultado:
never
👉 Pick<T, never> → {}
🧠 ¿Y por qué TS se queja?
Porque el predicate dice:
value is S
👉 pero value es:
T[keyof T]
👉 entonces TS exige:
“Si decís que
valuepuede serS, entoncesSdebe ser compatible conT[keyof T]”
🔥 Diferencia conceptual clave
| Caso | Uso de S | TS exige restricción |
|---|---|---|
| Arrays | solo en valores | ❌ no |
| ObjectValues | afecta estructura (Pick) | ✅ sí |
🧠 Forma de pensarlo
🟢 Arrays
(value) => value is S
👉 solo cambia el contenido del array
🔴 ObjectValues
Pick<T, KeysMatching<T, S>>
👉 cambia la forma del objeto
⚠️ Regla mental importante
Si un type parameter:
- solo afecta valores → TS es permisivo
- afecta estructura del tipo → TS exige restricciones
🧪 Analogía simple
Esto está bien:
const x = value as S;
Esto requiere chequeo:
type K = KeysMatching<T, S>;
👉 porque cambia el tipo estructural
🚀 Bonus insight (muy fino)
Tu versión de arrays:
(S | undefined)[]
👉 es “unsafe pero permitido”
Porque podrías hacer:
value is Date
aunque nunca haya Dates
👉 TS lo permite porque no rompe el tipo de T
🧩 TL;DR final
-
filterObjectArraysTyped✔ no usaSpara modificar estructura → no error -
filterObjectValuesTyped❗ usaSenPick→ requiere:
S extends T[keyof T]
Si querés, el siguiente nivel es mostrarte:
👉 cómo hacer una versión 100% segura sin as
👉 o cómo hacer narrowing por key individual (esto ya es nivel TypeScript ninja 🥷)