
El operador keyof
El operador keyof toma un tipo de objeto y produce una union de literales de cadena (o numéricos) que representan sus claves:
type Usuario = {
nombre: string
edad: number
email: string
}
type ClavesUsuario = keyof Usuario
// "nombre" | "edad" | "email"
Este tipo union restringe que solo se puedan usar claves válidas del objeto original:
function obtenerPropiedad(usuario: Usuario, clave: keyof Usuario) {
return usuario[clave]
}
const usuario: Usuario = { nombre: "Ana", edad: 28, email: "ana@ejemplo.com" }
obtenerPropiedad(usuario, "nombre")
// obtenerPropiedad(usuario, "telefono") // Error: "telefono" no es clave de Usuario
Keyof con genéricos
La combinación de keyof con genéricos permite crear funciones que preservan el tipo de retorno según la clave accedida:
function obtenerValor<T, K extends keyof T>(objeto: T, clave: K): T[K] {
return objeto[clave]
}
const config = {
tema: "oscuro",
idioma: "es",
notificaciones: true
}
const tema = obtenerValor(config, "tema")
// tipo: string
const notificaciones = obtenerValor(config, "notificaciones")
// tipo: boolean
La restricción K extends keyof T garantiza que K solo pueda ser una clave válida de T. El tipo de retorno T[K] es un tipo de acceso indexado que representa el tipo de la propiedad seleccionada.
Función de actualización segura
function actualizarPropiedad<T, K extends keyof T>(
objeto: T,
clave: K,
valor: T[K]
): T {
return { ...objeto, [clave]: valor }
}
const usuario = { nombre: "Ana", edad: 28, activo: true }
const actualizado = actualizarPropiedad(usuario, "edad", 29)
// actualizarPropiedad(usuario, "edad", "treinta") // Error: string no es number
Keyof con index signatures
Cuando un tipo tiene una index signature, keyof devuelve el tipo del índice:
type Diccionario = {
[clave: string]: number
}
type ClaveDiccionario = keyof Diccionario
// string | number (porque obj[0] equivale a obj["0"] en JavaScript)
type ArrayLike = {
[indice: number]: string
}
type ClaveArray = keyof ArrayLike
// number
Tipos de acceso indexado T[K]
Los tipos de acceso indexado permiten obtener el tipo de una propiedad específica de otro tipo usando la sintaxis de corchetes:
type Persona = {
nombre: string
direccion: {
calle: string
ciudad: string
codigoPostal: number
}
hobbies: string[]
}
type TipoDireccion = Persona["direccion"]
// { calle: string; ciudad: string; codigoPostal: number }
type TipoHobbies = Persona["hobbies"]
// string[]
type TipoCiudad = Persona["direccion"]["ciudad"]
// string
Se pueden usar uniones como índice para obtener la union de los tipos correspondientes:
type NombreOEdad = Persona["nombre" | "hobbies"]
// string | string[]
La combinación con keyof obtiene la union de todos los tipos de valor de un objeto:
type ValoresPersona = Persona[keyof Persona]
// string | { calle: string; ciudad: string; codigoPostal: number } | string[]
Acceso indexado con number
Para obtener el tipo de los elementos de un array, se usa number como índice:
const COLORES = ["rojo", "verde", "azul"] as const
type Color = typeof COLORES[number]
// "rojo" | "verde" | "azul"
El índice en un tipo de acceso indexado debe ser un tipo, no un valor. No se puede usar una variable const como índice directamente, pero si un type alias.
El operador typeof en posición de tipo
TypeScript añade un operador typeof que funciona en posiciones de tipo para extraer el tipo de un valor:
const configuracion = {
host: "localhost",
puerto: 5432,
tls: false
}
type TipoConfig = typeof configuracion
// { host: string; puerto: number; tls: boolean }
Esto es especialmente útil cuando el valor es la fuente de verdad y se quiere derivar un tipo a partir de el, en lugar de definir un tipo primero:
function crearConexion(host: string, puerto: number) {
return { host, puerto, conectado: false }
}
type Conexion = ReturnType<typeof crearConexion>
// { host: string; puerto: number; conectado: boolean }
Combinación de keyof y typeof
La combinación keyof typeof es un patrón frecuente para extraer las claves de un objeto en tiempo de ejecución como tipo:
const CODIGOS_HTTP = {
OK: 200,
CREADO: 201,
NO_ENCONTRADO: 404,
ERROR_SERVIDOR: 500
}
type NombreCodigo = keyof typeof CODIGOS_HTTP
// "OK" | "CREADO" | "NO_ENCONTRADO" | "ERROR_SERVIDOR"
type ValorCodigo = typeof CODIGOS_HTTP[keyof typeof CODIGOS_HTTP]
// number (200 | 201 | 404 | 500 si se usa as const)
Con as const se obtienen los valores literales:
const CODIGOS = {
OK: 200,
CREADO: 201,
NO_ENCONTRADO: 404,
ERROR_SERVIDOR: 500
} as const
type CodigoHTTP = typeof CODIGOS[keyof typeof CODIGOS]
// 200 | 201 | 404 | 500
typeof con funciones
typeof permite extraer el tipo de una función para usarlo con tipos utilitarios:
function procesar(datos: string[], opciones: { paralelo: boolean }) {
return { resultado: datos.length, exitoso: true }
}
type ParamsProcesar = Parameters<typeof procesar>
// [datos: string[], opciones: { paralelo: boolean }]
type ResultadoProcesar = ReturnType<typeof procesar>
// { resultado: number; exitoso: boolean }
Mapped types
Los mapped types crean nuevos tipos transformando sistemáticamente las propiedades de un tipo existente. La sintaxis usa in para iterar sobre un conjunto de claves:
type Opcional<T> = {
[K in keyof T]?: T[K]
}
interface Usuario {
nombre: string
edad: number
activo: boolean
}
type UsuarioOpcional = Opcional<Usuario>
// { nombre?: string; edad?: number; activo?: boolean }
Modificadores readonly y optional
Los mapped types soportan los modificadores readonly y ?, y permiten anadirlos o eliminarlos con los prefijos + y -:
type SoloLectura<T> = {
readonly [K in keyof T]: T[K]
}
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
type Requerido<T> = {
[K in keyof T]-?: T[K]
}
El prefijo - elimina el modificador existente. Sin prefijo, se asume +:
interface ConfigInmutable {
readonly host: string
readonly puerto: number
}
type ConfigMutable = Mutable<ConfigInmutable>
// { host: string; puerto: number } (sin readonly)
interface DatosOpcionales {
nombre?: string
email?: string
}
type DatosRequeridos = Requerido<DatosOpcionales>
// { nombre: string; email: string } (sin ?)
Transformación de tipos de valor
Los mapped types pueden cambiar el tipo de cada propiedad usando tipos condicionales:
type StringificarNumeros<T> = {
[K in keyof T]: T[K] extends number ? string : T[K]
}
interface Producto {
id: number
nombre: string
precio: number
disponible: boolean
}
type ProductoDTO = StringificarNumeros<Producto>
// { id: string; nombre: string; precio: string; disponible: boolean }
Un patrón común es hacer que todas las propiedades acepten null:
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
type ProductoNullable = Nullable<Producto>
// { id: number | null; nombre: string | null; precio: number | null; disponible: boolean | null }
Remapeo de claves con as
A partir de TypeScript 4.1, la cláusula as permite transformar las claves durante la iteración:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type GettersUsuario = Getters<Usuario>
// {
// getNombre: () => string
// getEdad: () => number
// getActivo: () => boolean
// }
Filtrado de propiedades con never
Producir never como clave en un remapeo excluye esa propiedad del tipo resultante:
type SoloStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
type StringsUsuario = SoloStrings<Usuario>
// { nombre: string }
type ExcluirPropiedades<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
}
type SinBooleans = ExcluirPropiedades<Usuario, boolean>
// { nombre: string; edad: number }
Mapped types sobre uniones arbitrarias
Los mapped types no están limitados a claves de objetos. Se puede iterar sobre cualquier union:
type EventosPermitidos = "click" | "hover" | "submit"
type ManejadoresEvento = {
[K in EventosPermitidos as `on${Capitalize<K>}`]: (evento: Event) => void
}
// {
// onClick: (evento: Event) => void
// onHover: (evento: Event) => void
// onSubmit: (evento: Event) => void
// }
Patrones avanzados de programación a nivel de tipos
Pick y Omit manuales
Los utility types Pick y Omit se pueden implementar con mapped types:
type MiPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type MiOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}
type InfoBasica = MiPick<Usuario, "nombre" | "email">
// { nombre: string; email: string } -- asumiendo email en Usuario
type SinEdad = MiOmit<Usuario, "edad">
// { nombre: string; activo: boolean }
Record tipado
El utility type Record también es un mapped type:
type MiRecord<K extends string | number | symbol, V> = {
[P in K]: V
}
type TablaPermisos = MiRecord<"leer" | "escribir" | "eliminar", boolean>
// { leer: boolean; escribir: boolean; eliminar: boolean }
Tipo DeepReadonly recursivo
Los mapped types permiten transformaciones recursivas:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
interface Estado {
usuario: {
nombre: string
preferencias: {
tema: string
idioma: string
}
}
contador: number
}
type EstadoInmutable = DeepReadonly<Estado>
// Todas las propiedades, incluyendo las anidadas, son readonly
Tipo para formularios
Un ejemplo práctico que combina keyof, mapped types y tipos condicionales:
interface DatosFormulario {
nombre: string
email: string
edad: number
acepta: boolean
}
type ErroresFormulario<T> = {
[K in keyof T]?: string
}
type EstadoCampos<T> = {
[K in keyof T]: {
valor: T[K]
tocado: boolean
error?: string
}
}
type CamposFormulario = EstadoCampos<DatosFormulario>
// {
// nombre: { valor: string; tocado: boolean; error?: string }
// email: { valor: string; tocado: boolean; error?: string }
// edad: { valor: number; tocado: boolean; error?: string }
// acepta: { valor: boolean; tocado: boolean; error?: string }
// }
function validarCampo<T, K extends keyof T>(
campos: EstadoCampos<T>,
clave: K
): string | undefined {
const campo = campos[clave]
if (!campo.tocado) return undefined
if (campo.valor === "" || campo.valor === null) {
return `El campo ${String(clave)} es requerido`
}
return undefined
}
Lookup types para API tipada
Combinando todos los operadores, se puede construir un sistema de tipos que modele una API completa:
type Endpoints = {
"/usuarios": {
GET: { respuesta: { id: string; nombre: string }[] }
POST: { cuerpo: { nombre: string; email: string }; respuesta: { id: string } }
}
"/productos": {
GET: { respuesta: { id: string; titulo: string; precio: number }[] }
}
}
type MetodosDeRuta<R extends keyof Endpoints> = keyof Endpoints[R]
type Respuesta<
R extends keyof Endpoints,
M extends keyof Endpoints[R]
> = Endpoints[R][M] extends { respuesta: infer Resp } ? Resp : never
type RespUsuariosGET = Respuesta<"/usuarios", "GET">
// { id: string; nombre: string }[]
type RespUsuariosPOST = Respuesta<"/usuarios", "POST">
// { id: string }
La combinación de keyof, tipos de acceso indexado, typeof y mapped types forma el nucleo de la metaprogramación de tipos en TypeScript, permitiendo crear abstracciones de tipo que se adaptan automáticamente a los cambios en las estructuras de datos subyacentes.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en TypeScript
Documentación oficial de TypeScript
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, TypeScript es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de TypeScript
Explora más contenido relacionado con TypeScript y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Dominar el operador keyof para extraer claves como tipos union, los tipos de acceso indexado T[K], el operador typeof para extraer tipos de valores, la combinación de keyof con typeof, los mapped types con modificadores y remapeo de claves, y patrones de programación a nivel de tipos.