learning-front

Nivel 5 · TypeScript: JavaScript con red de seguridad

typeof, keyof y acceso indexado

Cómo derivar tipos a partir de valores con typeof, recorrer las claves de un tipo con keyof y extraer el tipo de un campo con el acceso indexado T[K].

Hasta ahora has escrito tipos de dos formas: a mano (interface Heroe { ... }) o dejando que TypeScript los infiera. Pero hay un tercer camino: derivar un tipo a partir de un valor que ya existe. Si tienes un objeto con las constantes de tu aplicación, ¿por qué duplicar su estructura en un interface paralelo que puede quedarse desfasado?

Eso es lo que permiten typeof como operador de tipo, keyof y el acceso indexado T[K]. Son las herramientas que hacen que una sola fuente de verdad baste para todo. Y no es accidental que sean también el motor interno de los Pick, Omit, Record y demás utility types que verás en el siguiente capítulo.

typeof como operador de tipo#

Conoces typeof del capítulo de narrowing: if (typeof x === "number"). Ese typeof corre en ejecución y devuelve una cadena como "number" o "object".

Pero typeof en posición de tipo (tras : o en un type alias) es distinto: corre en compilación y captura el tipo estático de un valor. No devuelve ninguna cadena; devuelve un tipo.

typescript
// Un objeto de configuración: existe en el mundo de los VALORES.
const CONFIG = {
  maxPartidas: 200,
  version: "2026",
  activo: true,
};

// typeof CONFIG en posición de tipo captura su forma estática.
// Equivale a escribir: { maxPartidas: number; version: string; activo: boolean }
// pero sin duplicar la estructura a mano.
type ConfiguracionEquipo = typeof CONFIG;

El punto clave es este: si añades un campo a CONFIG, ConfiguracionEquipo lo recoge automáticamente. No hay dos lugares que mantener en sincronía.

Y el typeof de narrowing sigue siendo el mismo de siempre — solo cambia el contexto donde aparece:

typescript
// Posición de tipo (compilación): da el tipo estático de CONFIG.
type ConfiguracionEquipo = typeof CONFIG;

// Posición de valor (ejecución): devuelve la cadena "number".
if (typeof CONFIG.maxPartidas === "number") { ... }

keyof: las claves de un tipo como unión#

keyof T produce la unión de las claves de un tipo como literales de string. No es un valor que exista en ejecución: es un tipo que describe qué cadenas son claves válidas de T.

typescript
interface Heroe {
  nombre: string;
  rol: string;
  partidas: number;
  victorias: number;
}

// "nombre" | "rol" | "partidas" | "victorias"
type CampoHeroe = keyof Heroe;

¿Y qué sirve para esto? Para tipar cualquier función que recibe una clave del objeto. Sin keyof, el parámetro sería string y cualquier cadena inventada pasaría el chequeo. Con keyof, pasar una clave que no existe en Heroe es un error de compilación:

typescript
// Solo acepta claves reales de Heroe. "inventado" da error antes de ejecutar.
function etiquetarCampo(clave: keyof Heroe): string {
  return "Campo: " + clave;
}
etiquetarCampo("victorias"); // OK
// etiquetarCampo("inventado"); // Error

Acceso indexado T[K]#

El acceso indexado extrae el tipo de una propiedad de otro tipo. La sintaxis es la misma que para acceder a un campo de un objeto, pero en posición de tipo:

typescript
// Heroe["victorias"] da el tipo de esa propiedad: number.
type TipoVictorias = Heroe["victorias"];

// Heroe["nombre"] da string.
type TipoNombre = Heroe["nombre"];

El gran uso práctico es combinarlo con keyof en un genérico. El getter de abajo usa K extends keyof Heroe para garantizar que la clave existe, y Heroe[K] como tipo de retorno para que el resultado sea exactamente el tipo de esa propiedad, no unknown:

typescript
// K extends keyof Heroe: solo claves reales de Heroe.
// Heroe[K]: el tipo de retorno es el tipo exacto de la propiedad K.
function get<K extends keyof Heroe>(h: Heroe, k: K): Heroe[K] {
  return h[k];
}

// TypeScript infiere K en cada llamada.
// get(tracer, "victorias") → K = "victorias" → retorno: number
// get(tracer, "nombre")    → K = "nombre"    → retorno: string

¿Y qué cambia frente a devolver unknown? Que el resultado se puede usar directamente con su tipo correcto, sin aserción ni comprobación extra. Y si pides una clave que no existe, el error aparece en compilación, no en producción.

También puedes usar la unión completa de claves: T[keyof T] da la unión de todos los tipos de los valores:

typescript
// string | number: los tipos posibles de cualquier campo de Heroe.
type ValorHeroe = Heroe[keyof Heroe];

Combinar todo: typeof array[number]#

En el capítulo de as const se usó un array de literales para definir los roles del equipo. Ahora puedes cerrar el ciclo: en vez de escribir la unión "Daño" | "Apoyo" | "Tanque" a mano, se puede derivar del propio array.

El truco tiene dos pasos:

  1. as const convierte el array en readonly ["Daño", "Apoyo", "Tanque"] con literales exactos (no string[]).
  2. typeof roles[number] aplica el acceso indexado con number como índice, lo que da la unión de todos los elementos del array: "Daño" | "Apoyo" | "Tanque".
typescript
const roles = ["Daño", "Apoyo", "Tanque"] as const;

// typeof roles[number] → "Daño" | "Apoyo" | "Tanque"
// Si añades "Soporte" al array, Rol se actualiza solo.
type Rol = typeof roles[number];

¿Por qué importa? Porque así hay una sola fuente de verdad: el array. Si el diseño del juego cambia y se añade un nuevo rol, solo se toca el array y todos los tipos que dependen de él se actualizan solos. Una unión escrita a mano en dos sitios distintos tarde o temprano se desincroniza.

Los dos mundos#

TypeScript separa el mundo de los valores (lo que existe en ejecución: variables, objetos, funciones) del mundo de los tipos (lo que solo existe en compilación: interface, type, anotaciones). typeof, keyof y el acceso indexado son los puentes que van del mundo de los valores al de los tipos:

  • typeof valor → captura el tipo de un valor.
  • keyof Tipo → extrae las claves de un tipo como unión.
  • Tipo[Clave] → extrae el tipo de una propiedad.

Y eso es exactamente lo que hacen por dentro los utility types que verás en el próximo capítulo: Pick<T, K> usa keyof T para asegurarse de que K son claves reales; ReturnType<F> usa typeof para capturar el tipo de retorno de una función. Ya no serán magia.

Comprueba lo que sabes#

Pregunta 1 de 4

`typeof tracer` en posición de tipo, ¿qué hace?

Tu turno#

Tienes un objeto CONFIG con las constantes del Team Builder y una interfaz Heroe que los usa. Completa los cinco TODOs para derivar los tipos desde CONFIG en vez de escribirlos a mano, y convierte el getter en genérico con keyof y acceso indexado. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Deriva los tipos del Team Builder desde una fuente de verdad

Tienes un objeto CONFIG con las constantes del equipo y una interfaz Heroe que los usa. Tu trabajo es derivar los tipos desde CONFIG con typeof, keyof y acceso indexado, en vez de escribirlos en paralelo a mano. Completa los cinco TODOs hasta que el panel "Problemas" quede vacío y el getter devuelva tipos precisos.

Paso 1: Que funcione

  • Usas typeof CONFIG para obtener ConfiguracionEquipo sin escribir el tipo a mano.
  • Usas keyof ConfiguracionEquipo para ClavesConfig.
  • Usas acceso indexado ["maxPartidas"] para LimitePartidas.
  • El tipo de rol en Heroe sigue escrito a mano como unión literal.
  • El getter aún devuelve unknown.
Ver soluciones
// Solución OK: usa typeof para derivar un tipo a partir de un valor existente.
// Ya no escribimos el tipo de CONFIG a mano: TypeScript lo deduce del objeto.

const CONFIG = {
  roles: ["Daño", "Apoyo", "Tanque"] as const,
  maxPartidas: 200,
  version: "2026",
};

// typeof CONFIG captura el tipo estático del objeto: sus campos y sus tipos exactos.
// Si CONFIG cambia, ConfiguracionEquipo se actualiza solo. Cero duplicación.
type ConfiguracionEquipo = typeof CONFIG;

// keyof ConfiguracionEquipo da la unión de sus claves como literales de string.
// Resultado: "roles" | "maxPartidas" | "version"
type ClavesConfig = keyof ConfiguracionEquipo;

// Acceso indexado: el tipo del campo "maxPartidas" dentro de ConfiguracionEquipo.
// Resultado: number
type LimitePartidas = ConfiguracionEquipo["maxPartidas"];

interface Heroe {
  nombre: string;
  // El tipo del campo "rol" se escribe a mano aquí: es el primer paso, funciona,
  // pero si el array cambia habría que actualizarlo en dos sitios.
  rol: "Daño" | "Apoyo" | "Tanque";
  partidas: number;
  victorias: number;
}

// El getter genérico aún no usa keyof ni acceso indexado: devuelve unknown.
// Funciona, pero el tipo de retorno se pierde en quien lo llama.
function get(h: Heroe, k: string): unknown {
  return h[k as keyof Heroe];
}

const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo", partidas: 180, victorias: 155 };

const nombreTracer = get(tracer, "nombre");
const victoriasTracer = get(tracer, "victorias");
console.log("Nombre: " + nombreTracer);
console.log("Victorias: " + victoriasTracer);
console.log("Limite de partidas en config: " + CONFIG.maxPartidas);

Por qué este nivel

  • typeof CONFIG deriva ConfiguracionEquipo sin duplicar la estructura: si el objeto cambia, el tipo se actualiza solo. El getter sigue con unknown pero los tipos de configuración ya son correctos.
  • keyof y el acceso indexado ["maxPartidas"] muestran la mecánica básica: del objeto al tipo, del tipo a un campo concreto.
  • El tipo de rol aún está escrito a mano como "Daño" | "Apoyo" | "Tanque". Es el paso pendiente para llegar a excelente: si el array cambia, la unión queda desfasada.