Operador keyof
El operador keyof es una herramienta fundamental en TypeScript que permite extraer las claves de un tipo de objeto como un tipo de unión literal. Su funcionamiento es directo: toma un tipo de objeto y produce un tipo que representa todas sus propiedades como literales de cadena.
La sintaxis básica utiliza la palabra clave keyof
seguida del tipo del cual queremos extraer las claves:
type Usuario = {
nombre: string;
edad: number;
email: string;
};
type ClavesUsuario = keyof Usuario; // "nombre" | "edad" | "email"
Uso con interfaces
El operador keyof funciona perfectamente con interfaces, manteniendo la misma sintaxis:
interface Producto {
id: number;
titulo: string;
precio: number;
disponible: boolean;
}
type PropiedadesProducto = keyof Producto; // "id" | "titulo" | "precio" | "disponible"
// Podemos usar este tipo para crear funciones que solo acepten claves válidas
function obtenerPropiedad(producto: Producto, clave: PropiedadesProducto) {
return producto[clave];
}
const producto: Producto = {
id: 1,
titulo: "Laptop",
precio: 899,
disponible: true
};
const titulo = obtenerPropiedad(producto, "titulo"); // ✅ Válido
// const invalido = obtenerPropiedad(producto, "stock"); // ❌ Error: no existe la propiedad
Combinación con generics
Una de las aplicaciones más útiles del operador keyof es su combinación con tipos genéricos para crear funciones flexibles y seguras:
function obtenerValor<T, K extends keyof T>(objeto: T, clave: K): T[K] {
return objeto[clave];
}
const configuracion = {
tema: "oscuro",
idioma: "es",
notificaciones: true
};
const tema = obtenerValor(configuracion, "tema"); // tipo: string
const idioma = obtenerValor(configuracion, "idioma"); // tipo: string
const notificaciones = obtenerValor(configuracion, "notificaciones"); // tipo: boolean
En este ejemplo, K extends keyof T
garantiza que K
solo puede ser una clave válida de T
, mientras que el tipo de retorno T[K]
representa exactamente el tipo de la propiedad seleccionada.
Trabajar con objetos dinámicos
El operador keyof resulta especialmente valioso cuando trabajamos con objetos cuya estructura puede variar:
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 usuarioActualizado = actualizarPropiedad(usuario, "edad", 29);
// const error = actualizarPropiedad(usuario, "edad", "treinta"); // ❌ Error: tipo incorrecto
Keyof con tipos primitivos
Cuando aplicamos keyof a tipos primitivos, obtenemos las claves de sus prototipos correspondientes:
type ClavesString = keyof string; // Incluye métodos como "charAt", "slice", "toLowerCase", etc.
type ClavesNumber = keyof number; // Incluye métodos como "toString", "toFixed", "valueOf", etc.
type ClavesArray = keyof Array<any>; // Incluye métodos como "push", "pop", "map", "filter", etc.
Sin embargo, en la práctica, es más común usar keyof con tipos de objeto definidos por el desarrollador.
Casos de uso prácticos
El operador keyof es fundamental para crear sistemas de validación y manipulación de datos seguros:
interface FormularioContacto {
nombre: string;
email: string;
mensaje: string;
}
type CamposFormulario = keyof FormularioContacto;
class ValidadorFormulario {
private errores: Partial<Record<CamposFormulario, string>> = {};
validarCampo(campo: CamposFormulario, valor: string): boolean {
if (!valor.trim()) {
this.errores[campo] = `El campo ${campo} es requerido`;
return false;
}
if (campo === "email" && !this.esEmailValido(valor)) {
this.errores[campo] = "Email inválido";
return false;
}
delete this.errores[campo];
return true;
}
private esEmailValido(email: string): boolean {
return email.includes("@");
}
}
Esta aproximación garantiza que solo podamos validar campos que realmente existen en nuestra interfaz, evitando errores tipográficos y mejorando la mantenibilidad del código.
Mapped types básicos
Los mapped types permiten crear nuevos tipos transformando sistemáticamente las propiedades de un tipo existente. Esta característica utiliza la sintaxis de indexación para iterar sobre las claves de un tipo y aplicar transformaciones específicas a cada propiedad.
La sintaxis básica de un mapped type sigue este patrón:
type NuevoTipo<T> = {
[K in keyof T]: TipoTransformado
}
Transformación de propiedades
El caso más simple consiste en transformar el tipo de todas las propiedades de un objeto:
interface Usuario {
nombre: string;
edad: number;
activo: boolean;
}
// Convertir todas las propiedades a string
type UsuarioComoString<T> = {
[K in keyof T]: string;
}
type UsuarioStringificado = UsuarioComoString<Usuario>;
// Resultado: { nombre: string; edad: string; activo: string; }
Propiedades opcionales
Podemos hacer que todas las propiedades sean opcionales usando el modificador ?
:
type Opcional<T> = {
[K in keyof T]?: T[K];
}
type UsuarioOpcional = Opcional<Usuario>;
// Resultado: { nombre?: string; edad?: number; activo?: boolean; }
// Ejemplo práctico para actualizaciones parciales
function actualizarUsuario(usuario: Usuario, cambios: UsuarioOpcional): Usuario {
return {
...usuario,
...cambios
};
}
const usuario: Usuario = { nombre: "Carlos", edad: 30, activo: true };
const usuarioActualizado = actualizarUsuario(usuario, { edad: 31 });
Propiedades de solo lectura
El modificador readonly se aplica de manera similar para crear versiones inmutables:
type SoloLectura<T> = {
readonly [K in keyof T]: T[K];
}
type UsuarioInmutable = SoloLectura<Usuario>;
// Resultado: { readonly nombre: string; readonly edad: number; readonly activo: boolean; }
const usuarioFijo: UsuarioInmutable = { nombre: "Elena", edad: 25, activo: true };
// usuarioFijo.nombre = "Ana"; // ❌ Error: no se puede asignar a una propiedad readonly
Preservación de tipos originales
Los mapped types pueden preservar los tipos originales mientras aplican modificadores:
interface Configuracion {
tema: "claro" | "oscuro";
idioma: "es" | "en" | "fr";
notificaciones: boolean;
maxIntentos: number;
}
type ConfiguracionOpcional = {
[K in keyof Configuracion]?: Configuracion[K];
}
// Los tipos literales se mantienen intactos
const configParcial: ConfiguracionOpcional = {
tema: "oscuro", // ✅ Solo acepta "claro" | "oscuro"
notificaciones: true
// idioma y maxIntentos son opcionales
};
Transformación condicional básica
Podemos aplicar transformaciones condicionales simples usando tipos condicionales:
type StringificarNumeros<T> = {
[K in keyof T]: T[K] extends number ? string : T[K];
}
interface Datos {
id: number;
nombre: string;
precio: number;
disponible: boolean;
}
type DatosStringificados = StringificarNumeros<Datos>;
// Resultado: { id: string; nombre: string; precio: string; disponible: boolean; }
Combinar con keyof para crear utilidades
Los mapped types se combinan naturalmente con keyof para crear herramientas de manipulación de tipos:
type CamposPorDefecto<T> = {
[K in keyof T]: T[K] | null;
}
interface Producto {
nombre: string;
categoria: string;
precio: number;
}
type ProductoConDefectos = CamposPorDefecto<Producto>;
// Resultado: { nombre: string | null; categoria: string | null; precio: number | null; }
// Función que inicializa con valores por defecto
function crearProductoVacio(): ProductoConDefectos {
return {
nombre: null,
categoria: null,
precio: null
};
}
Renombrado de propiedades
Los mapped types permiten renombrar propiedades usando template literal types:
type ConPrefijo<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
}
interface Medidas {
ancho: number;
alto: number;
}
type MedidasEnPixeles = ConPrefijo<Medidas, "px">;
// Resultado: { pxancho: number; pxalto: number; }
Casos prácticos de uso
Los mapped types resultan especialmente útiles para crear APIs flexibles:
interface EstadoFormulario {
nombre: string;
email: string;
edad: number;
}
// Tipo para manejar errores de validación
type ErroresValidacion<T> = {
[K in keyof T]?: string;
}
// Tipo para manejar estado de carga
type EstadoCarga<T> = {
[K in keyof T]: boolean;
}
class ManejadorFormulario {
private valores: EstadoFormulario = { nombre: "", email: "", edad: 0 };
private errores: ErroresValidacion<EstadoFormulario> = {};
private cargando: EstadoCarga<EstadoFormulario> = {
nombre: false,
email: false,
edad: false
};
establecerError(campo: keyof EstadoFormulario, mensaje: string): void {
this.errores[campo] = mensaje;
}
establecerCarga(campo: keyof EstadoFormulario, estado: boolean): void {
this.cargando[campo] = estado;
}
}
Esta aproximación garantiza que los tipos de errores y estados de carga coincidan exactamente con los campos del formulario, manteniendo la coherencia del sistema de tipos incluso cuando la estructura del formulario evoluciona.
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
- Comprender el funcionamiento del operador keyof para extraer claves de tipos de objetos.
- Aplicar keyof con interfaces y tipos genéricos para funciones seguras.
- Crear mapped types para transformar propiedades de tipos existentes.
- Utilizar modificadores como readonly y opcional en mapped types.
- Combinar keyof y mapped types para construir utilidades y APIs flexibles.