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.
// 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:
// 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.
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:
// 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"); // ErrorAcceso 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:
// 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:
// 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:
// 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:
as constconvierte el array enreadonly ["Daño", "Apoyo", "Tanque"]con literales exactos (nostring[]).typeof roles[number]aplica el acceso indexado connumbercomo índice, lo que da la unión de todos los elementos del array:"Daño" | "Apoyo" | "Tanque".
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.
Paso 2: Que sea preciso
- El getter es genérico: function get<K extends keyof Heroe>(h: Heroe, k: K): Heroe[K].
- Pedir get(tracer, "victorias") devuelve number; pedir get(tracer, "nombre") devuelve string.
- Pedir una clave inexistente es error de compilación, no de ejecución.
Paso 3: Que sea una fuente de verdad
- type Rol = typeof CONFIG.roles[number] deriva la unión desde el array as const.
- Heroe.rol usa Rol en vez de la unión escrita a mano.
- Si añades un rol al array CONFIG.roles, el tipo Rol y Heroe.rol se actualizan solos. Cero duplicación.
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.
// Solución mejor: getter genérico tipado con keyof y acceso indexado.
// El tipo de retorno ya no es unknown: TypeScript lo infiere según la clave que pases.
const CONFIG = {
roles: ["Daño", "Apoyo", "Tanque"] as const,
maxPartidas: 200,
version: "2026",
};
// typeof CONFIG captura el tipo del objeto: la fuente de verdad de la configuración.
type ConfiguracionEquipo = typeof CONFIG;
// keyof da la unión de claves: "roles" | "maxPartidas" | "version"
type ClavesConfig = keyof ConfiguracionEquipo;
// Acceso indexado: el tipo del campo "maxPartidas" — resulta number.
type LimitePartidas = ConfiguracionEquipo["maxPartidas"];
interface Heroe {
nombre: string;
// El tipo de "rol" sigue escrito a mano. Mejora pendiente para el nivel excelente.
rol: "Daño" | "Apoyo" | "Tanque";
partidas: number;
victorias: number;
}
// Getter genérico con restricción keyof y acceso indexado en el retorno.
// K extends keyof Heroe garantiza que solo se pueden pedir claves reales de Heroe.
// Heroe[K] dice: "el tipo de retorno es el tipo de la propiedad K en Heroe".
// Si pides "victorias" el retorno es number; si pides "nombre" es string. Exacto.
function get<K extends keyof Heroe>(h: Heroe, k: K): Heroe[K] {
return h[k];
}
const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo", partidas: 180, victorias: 155 };
// Ahora el tipo de retorno es string, no unknown.
const nombreTracer = get(tracer, "nombre");
// Y aquí es number: se puede operar directamente.
const victoriasTracer = get(tracer, "victorias");
console.log("Nombre: " + nombreTracer);
console.log("Victorias: " + victoriasTracer);
// Pedir una clave que no existe en Heroe es un error de compilación, no de ejecución.
// get(tracer, "inventado"); // Error: Argument of type '"inventado"' is not assignable to keyof Heroe.
console.log("Limite de partidas: " + CONFIG.maxPartidas); Por qué es mejor que el anterior
- El getter genérico con K extends keyof Heroe y Heroe[K] en el retorno es el salto más importante: el tipo de retorno deja de ser unknown y se preserva exactamente según la clave pedida.
- TypeScript infiere K en cada llamada: get(tracer, "victorias") → K = "victorias" → retorno number. El usuario no escribe el genérico; lo infiere el compilador.
- Pedir una clave inexistente es error de compilación. Antes de este cambio ese error no aparecía hasta ejecución (o nunca, con suerte).
// Solución excelente: única fuente de verdad para todo.
// El tipo Rol se deriva del array as const: si añades un rol al array, el tipo se actualiza solo.
const CONFIG = {
roles: ["Daño", "Apoyo", "Tanque"] as const,
maxPartidas: 200,
version: "2026",
};
// typeof CONFIG captura el tipo exacto, incluido el array de literales.
type ConfiguracionEquipo = typeof CONFIG;
// keyof da la unión de claves del objeto de configuración.
type ClavesConfig = keyof ConfiguracionEquipo;
// Acceso indexado al campo maxPartidas: da number.
type LimitePartidas = ConfiguracionEquipo["maxPartidas"];
// typeof CONFIG.roles da el tipo del array as const: readonly ["Daño", "Apoyo", "Tanque"].
// El acceso indexado [number] da la unión de sus elementos: "Daño" | "Apoyo" | "Tanque".
// Si añades "Soporte" al array de CONFIG.roles, Rol se actualiza solo. Cero duplicación.
type Rol = typeof CONFIG.roles[number];
interface Heroe {
nombre: string;
// rol usa el tipo derivado: si el array cambia, este campo también se actualiza.
rol: Rol;
partidas: number;
victorias: number;
}
// Getter genérico: keyof garantiza claves reales, acceso indexado preserva el tipo de retorno.
function get<K extends keyof Heroe>(h: Heroe, k: K): Heroe[K] {
return h[k];
}
const tracer: Heroe = { nombre: "Tracer", rol: "Daño", partidas: 120, victorias: 78 };
const mercy: Heroe = { nombre: "Mercy", rol: "Apoyo", partidas: 180, victorias: 155 };
// El tipo de retorno se preserva: string para "nombre", number para "victorias".
const nombreTracer = get(tracer, "nombre");
const victoriasTracer = get(tracer, "victorias");
console.log("Nombre: " + nombreTracer);
console.log("Victorias: " + victoriasTracer);
// Heroe[keyof Heroe] da la union de los tipos de TODOS los valores: string | number.
// Util para funciones que manejan cualquier campo del heroe sin saber cuál de antemano.
type ValorHeroe = Heroe[keyof Heroe];
console.log("Roles posibles: " + CONFIG.roles.join(", "));
console.log("Limite de partidas: " + CONFIG.maxPartidas);
// Asignar un rol que no está en el array es un error de compilación.
// const ana: Heroe = { nombre: "Ana", rol: "Curación", partidas: 90, victorias: 60 };
// Error: Type '"Curación"' is not assignable to type '"Daño" | "Apoyo" | "Tanque"'. Por qué es mejor que el anterior
- typeof CONFIG.roles[number] es la pieza que cierra el ciclo: el tipo Rol vive en el array, no en una unión escrita a mano. Añadir "Soporte" al array actualiza Rol y, por tanto, Heroe.rol sin tocar nada más.
- Heroe[keyof Heroe] como ValorHeroe muestra el acceso indexado con la unión completa de claves: string | number, los tipos posibles de cualquier campo del héroe.
- Todo el sistema parte de un único array de valores. Ningún tipo repite información que ya está en otro lado. Eso es lo que hace que este nivel sea "excelente": no solo funciona, sino que es imposible que se desincronice.