martes, 31 de marzo de 2026

Fórmulas tipadas para manipular keys de objetos

 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 }

👉 anever 💥


🚀 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 😄)



Ahora quiero una función que dado un objeto y un valor, me devuelva un array con cada key del objeto cuyo valor corresponde al valor dado. Quiero que tenga un buen nivel de tipado en Typescript.

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)


Quiero que soporte comparación custom (en particular, deepEqual).

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:

  1. Permitimos un comparador custom
  2. 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 deepEqual es 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)

¿Cómo funciona esto?

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

  1. Unión:
A | B
  1. Distribución:
(k: A) => void | (k: B) => void
  1. Inferencia:
(k: infer I) => void
  1. 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 😄)

Quiero una función que se parezca en cierto sentido a "filter" de la API de Javascript, pero a diferencia de esta, que el array devuelto contenga slots vacíos en donde en el array original no satisface la condición de filtro. Quiero que sea en Typescript con un nivel razonable de tipado.

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)

VarianteResultadoTipadoComportamiento
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 😄)

Ahora quiero una función que dado un objeto que todas sus propiedades son arrays, y dado un predicado, filtre todos los arrays con la función filterKeepShape y reemplazando cada array original con el array filtrado, y elimine las keys de aquellos arrays que todos sus elementos sean undefined. Lo quiero en Typescript con un nivel razonable de tipado.

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 😄)


Ahora quiero una función como filterObjectArraysTyped pero que sea filterObjectKeysTyped o filterObjectValuesTyped, y que en vez de arrays sean valores primitivos.

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)


El tipo de un predicado de tipo debe poderse asignar al tipo de su parámetro. El tipo 'S' no se puede asignar al tipo 'T[keyof T]'. "S" puede asignarse a la restricción de tipo "T[keyof T]", pero no se pudo crear una instancia de "T[keyof T]" con un subtipo distinto de la restricción "unknown".ts(2677) [archivo, línea y columna de declaración de S]: Este parámetro de tipo podría necesitar una restricción "extends T[keyof T]". (type parameter) S in filterObjectValuesTyped<T extends AnyRecord, S>(obj: T, predicate: (value: T[keyof T], key: keyof T) => value is S): Partial<{ [K in keyof T]: S; }>

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:

❌ “S no necesariamente es compatible con T[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 S solo es válido si S es compatible con el tipo original de value


🧪 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)

¿Pero por qué en filterObjectArraysTyped no daba ningún error? 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)[]; }>

Excelente observación—esto es una sutileza profunda de TypeScript 👀
y la diferencia no es obvia a primera vista.


🧠 TL;DR corto

En filterObjectArraysTyped no da error porque no estás usando S para restringir T
En filterObjectValuesTyped sí 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:

  • S no se usa para indexar T
  • no se usa en Pick, ni en keyof
  • 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 S para 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 value puede ser S, entonces S debe ser compatible con T[keyof T]


🔥 Diferencia conceptual clave

CasoUso de STS exige restricción
Arrayssolo en valores❌ no
ObjectValuesafecta 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 usa S para modificar estructura → no error
  • filterObjectValuesTyped ❗ usa S en Pick → 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 🥷)