TypeScript
Tutorial TypeScript: Tipos de utilidad (Partial, Required, Pick, etc)
Aprende los tipos de utilidad esenciales en TypeScript como Partial, Required, Pick, Omit, Readonly, Record, ReturnType y Parameters para tipado avanzado.
Aprende TypeScript y certifícatePartial y Required
TypeScript ofrece una serie de tipos de utilidad que nos permiten transformar tipos existentes para adaptarlos a nuestras necesidades específicas. Entre estos, Partial
y Required
son dos utilidades fundamentales que modifican la obligatoriedad de las propiedades de un tipo.
Tipo Partial
El tipo de utilidad Partial<T>
convierte todas las propiedades de un tipo en opcionales. Esto resulta extremadamente útil cuando necesitamos trabajar con objetos que pueden tener solo algunas de las propiedades de un tipo completo.
La definición interna de Partial
es bastante sencilla:
type Partial<T> = {
[P in keyof T]?: T[P];
};
Veamos un ejemplo práctico:
interface Usuario {
id: number;
nombre: string;
email: string;
edad: number;
}
// Sin Partial, necesitaríamos todas las propiedades
function actualizarUsuario(id: number, datosCompletos: Usuario) {
// Actualiza el usuario con todos los campos
}
// Con Partial, podemos actualizar solo algunas propiedades
function actualizarUsuarioParcial(id: number, datosParciales: Partial<Usuario>) {
// Actualiza solo las propiedades proporcionadas
}
// Esto daría error sin Partial
actualizarUsuarioParcial(1, {
nombre: "Ana Martínez"
});
En este ejemplo, Partial<Usuario>
nos permite pasar un objeto que contiene solo algunas de las propiedades de Usuario
, lo que es ideal para operaciones de actualización parcial como en APIs REST.
Casos de uso comunes para Partial
- Actualizaciones parciales de datos: Ideal para operaciones PATCH en APIs.
function actualizarProducto(id: string, cambios: Partial<Producto>): Promise<Producto> {
return fetch(`/api/productos/${id}`, {
method: 'PATCH',
body: JSON.stringify(cambios),
headers: { 'Content-Type': 'application/json' }
}).then(res => res.json());
}
// Actualizar solo el precio
actualizarProducto('prod-123', { precio: 29.99 });
- Opciones de configuración: Para parámetros opcionales en funciones de configuración.
interface OpcionesGrafico {
ancho: number;
alto: number;
colorFondo: string;
colorLinea: string;
mostrarEtiquetas: boolean;
mostrarLeyenda: boolean;
}
function crearGrafico(opciones: Partial<OpcionesGrafico> = {}) {
// Valores por defecto
const configuracionCompleta: OpcionesGrafico = {
ancho: 500,
alto: 300,
colorFondo: '#ffffff',
colorLinea: '#333333',
mostrarEtiquetas: true,
mostrarLeyenda: true,
...opciones // Sobreescribir con las opciones proporcionadas
};
// Crear gráfico con la configuración
}
// Usar con mínimas opciones
crearGrafico({ ancho: 800, alto: 600 });
Tipo Required
El tipo de utilidad Required<T>
hace exactamente lo contrario que Partial
: convierte todas las propiedades opcionales de un tipo en obligatorias. Esto es útil cuando necesitamos asegurarnos de que todas las propiedades estén presentes, incluso aquellas que originalmente eran opcionales.
La definición interna de Required
es:
type Required<T> = {
[P in keyof T]-?: T[P];
};
El operador -?
elimina el modificador de opcionalidad de las propiedades.
Veamos un ejemplo:
interface ConfiguracionApp {
tema?: string;
idioma?: string;
notificaciones?: boolean;
limiteIntentos?: number;
}
// Esta función requiere que todas las propiedades estén definidas
function inicializarAplicacion(config: Required<ConfiguracionApp>) {
// Podemos acceder a todas las propiedades con seguridad
console.log(`Iniciando con tema ${config.tema}, idioma ${config.idioma}`);
console.log(`Notificaciones: ${config.notificaciones}, límite: ${config.limiteIntentos}`);
}
// Esto daría error porque faltan propiedades
inicializarAplicacion({
tema: 'oscuro',
idioma: 'es'
});
// Esto es correcto
inicializarAplicacion({
tema: 'oscuro',
idioma: 'es',
notificaciones: true,
limiteIntentos: 3
});
Casos de uso comunes para Required
- Validación de datos completos: Cuando necesitamos asegurarnos de que todos los campos estén presentes.
interface FormularioContacto {
nombre?: string;
email?: string;
mensaje?: string;
}
function validarFormularioCompleto(datos: Required<FormularioContacto>): boolean {
return datos.nombre.length > 0 &&
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(datos.email) &&
datos.mensaje.length > 10;
}
- Transformación de datos: Para asegurarnos de que todos los campos opcionales tengan un valor por defecto.
interface OpcionesUsuario {
recibirEmails?: boolean;
mostrarAvatar?: boolean;
temaOscuro?: boolean;
}
function normalizarOpciones(opciones: Partial<OpcionesUsuario>): Required<OpcionesUsuario> {
return {
recibirEmails: opciones.recibirEmails ?? true,
mostrarAvatar: opciones.mostrarAvatar ?? true,
temaOscuro: opciones.temaOscuro ?? false
};
}
const opcionesCompletas = normalizarOpciones({ temaOscuro: true });
// Resultado: { recibirEmails: true, mostrarAvatar: true, temaOscuro: true }
Combinando Partial y Required
Estas utilidades pueden combinarse con otras para crear transformaciones más complejas:
interface Producto {
id: string;
nombre: string;
precio: number;
descripcion?: string;
imagenes?: string[];
stock?: number;
}
// Solo algunas propiedades son requeridas
type ProductoMinimo = Required<Pick<Producto, 'nombre' | 'precio'>>;
// Todas las propiedades son requeridas excepto id (que se genera automáticamente)
type ProductoNuevo = Required<Omit<Producto, 'id'>>;
function crearProducto(producto: ProductoNuevo): Producto {
return {
id: generarId(),
...producto
};
}
Implementación personalizada
Podemos entender mejor cómo funcionan estas utilidades implementándolas nosotros mismos:
// Nuestra propia implementación de Partial
type MiPartial<T> = {
[P in keyof T]?: T[P];
};
// Nuestra propia implementación de Required
type MiRequired<T> = {
[P in keyof T]-?: T[P];
};
interface Persona {
nombre: string;
apellido: string;
edad?: number;
direccion?: string;
}
// Ambas funcionarían igual que las utilidades nativas
type PersonaParcial = MiPartial<Persona>;
type PersonaCompleta = MiRequired<Persona>;
Estas implementaciones utilizan mapped types (tipos mapeados), una característica poderosa de TypeScript que permite transformar cada propiedad de un tipo según un patrón definido.
En resumen, Partial<T>
y Required<T>
son herramientas fundamentales en TypeScript que nos permiten modificar la obligatoriedad de las propiedades de un tipo, facilitando la creación de APIs flexibles y seguras. Mientras que Partial
hace que todas las propiedades sean opcionales, ideal para actualizaciones parciales, Required
garantiza que todas las propiedades estén presentes, perfecto para validaciones completas.
Pick y Omit
TypeScript proporciona tipos de utilidad que nos permiten crear nuevos tipos a partir de tipos existentes seleccionando o excluyendo propiedades específicas. Pick
y Omit
son dos herramientas fundamentales que nos ayudan a construir tipos más precisos y adaptados a nuestras necesidades concretas.
Tipo Pick<T, K>
El tipo de utilidad Pick<T, K>
nos permite seleccionar un subconjunto de propiedades de un tipo existente. Recibe dos parámetros genéricos:
T
: El tipo original del que queremos extraer propiedadesK
: Las claves (nombres de propiedades) que queremos seleccionar
La definición interna de Pick
es:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Veamos un ejemplo práctico:
interface Empleado {
id: number;
nombre: string;
apellido: string;
email: string;
salario: number;
departamento: string;
fechaContratacion: Date;
}
// Creamos un tipo que solo contiene información básica del empleado
type InformacionBasica = Pick<Empleado, 'id' | 'nombre' | 'apellido'>;
// Equivalente a:
// interface InformacionBasica {
// id: number;
// nombre: string;
// apellido: string;
// }
function mostrarListaEmpleados(empleados: InformacionBasica[]) {
empleados.forEach(emp => {
console.log(`${emp.id}: ${emp.nombre} ${emp.apellido}`);
});
}
En este ejemplo, Pick
nos permite crear un tipo más ligero que solo contiene las propiedades que necesitamos para mostrar una lista básica de empleados.
Casos de uso comunes para Pick
- Vistas simplificadas de datos: Cuando necesitamos versiones reducidas de tipos complejos.
interface Producto {
id: string;
nombre: string;
descripcion: string;
precio: number;
stock: number;
categoria: string;
imagenes: string[];
especificaciones: Record<string, string>;
}
// Para listados donde solo necesitamos información básica
type ProductoResumen = Pick<Producto, 'id' | 'nombre' | 'precio' | 'imagenes'>;
function mostrarCatalogo(productos: ProductoResumen[]) {
// Renderizar vista simplificada
}
- Formularios parciales: Para crear tipos que representen partes específicas de un formulario.
interface DatosUsuario {
nombre: string;
email: string;
password: string;
direccion: string;
telefono: string;
preferencias: Record<string, boolean>;
}
// Para el primer paso de un formulario de registro
type FormularioDatosBasicos = Pick<DatosUsuario, 'nombre' | 'email' | 'password'>;
function PrimerPasoRegistro({ onSubmit }: { onSubmit: (datos: FormularioDatosBasicos) => void }) {
// Componente de formulario con solo los campos básicos
}
Tipo Omit<T, K>
El tipo de utilidad Omit<T, K>
hace lo contrario que Pick
: crea un nuevo tipo excluyendo ciertas propiedades del tipo original. También recibe dos parámetros genéricos:
T
: El tipo originalK
: Las claves que queremos omitir
La definición interna de Omit
combina otros tipos de utilidad:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Veamos un ejemplo:
interface Articulo {
id: number;
titulo: string;
contenido: string;
autor: string;
fechaCreacion: Date;
fechaModificacion: Date;
publicado: boolean;
}
// Creamos un tipo para crear nuevos artículos (sin id ni fechas que se generan automáticamente)
type NuevoArticulo = Omit<Articulo, 'id' | 'fechaCreacion' | 'fechaModificacion'>;
function crearArticulo(articulo: NuevoArticulo): Articulo {
const ahora = new Date();
return {
...articulo,
id: generarId(),
fechaCreacion: ahora,
fechaModificacion: ahora
};
}
En este ejemplo, Omit
nos permite crear un tipo para nuevos artículos que no incluye campos que se generarán automáticamente en el servidor.
Casos de uso comunes para Omit
- Formularios de creación: Para omitir campos que se generan automáticamente.
interface Tarea {
id: string;
titulo: string;
descripcion: string;
completada: boolean;
fechaCreacion: Date;
fechaActualizacion: Date;
}
// Para crear nuevas tareas (sin id ni fechas)
type NuevaTarea = Omit<Tarea, 'id' | 'fechaCreacion' | 'fechaActualizacion'>;
function agregarTarea(tarea: NuevaTarea) {
const nuevaTarea: Tarea = {
...tarea,
id: crypto.randomUUID(),
fechaCreacion: new Date(),
fechaActualizacion: new Date()
};
return guardarEnBaseDeDatos(nuevaTarea);
}
- Eliminación de campos sensibles: Para crear versiones públicas de tipos que contienen información privada.
interface UsuarioCompleto {
id: number;
nombre: string;
email: string;
passwordHash: string;
intentosFallidos: number;
ultimoAcceso: Date;
rol: string;
}
// Versión pública sin datos sensibles
type UsuarioPublico = Omit<UsuarioCompleto, 'passwordHash' | 'intentosFallidos'>;
function obtenerPerfilPublico(id: number): UsuarioPublico {
const usuario = buscarUsuarioPorId(id);
// No necesitamos filtrar manualmente, el tipo ya lo hace
return usuario;
}
Combinando Pick y Omit con otros tipos de utilidad
Estas utilidades son aún más poderosas cuando se combinan entre sí o con otros tipos de utilidad:
interface Producto {
id: string;
nombre: string;
precio: number;
descripcion: string;
stock: number;
categoria: string;
}
// Crear un tipo donde solo precio y stock son editables
type ProductoEditable = Pick<Partial<Producto>, 'precio' | 'stock'>;
// Crear un tipo donde todas las propiedades son requeridas excepto descripción
type ProductoSinDescripcion = Required<Omit<Producto, 'descripcion'>>;
// Crear un tipo para actualizar un producto (sin id, todas opcionales)
type ActualizacionProducto = Partial<Omit<Producto, 'id'>>;
function actualizarProducto(id: string, datos: ActualizacionProducto) {
// Implementación
}
Implementación personalizada
Para entender mejor cómo funcionan estas utilidades, podemos implementarlas nosotros mismos:
// Nuestra propia implementación de Pick
type MiPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Nuestra propia implementación de Omit
type MiOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
interface Persona {
nombre: string;
apellido: string;
edad: number;
direccion: string;
telefono: string;
}
// Funcionarían igual que las utilidades nativas
type DatosContacto = MiPick<Persona, 'nombre' | 'telefono' | 'direccion'>;
type DatosSinContacto = MiOmit<Persona, 'telefono' | 'direccion'>;
Diferencias clave entre Pick y Omit
Aunque parezcan opuestos, hay algunas diferencias sutiles a tener en cuenta:
Pick
requiere que las claves seleccionadas existan en el tipo original (K extends keyof T
).Omit
permite especificar claves que no existen en el tipo original, ya que usaK extends keyof any
.
interface Usuario {
id: number;
nombre: string;
}
// Esto da error porque 'email' no existe en Usuario
type ConEmail = Pick<Usuario, 'id' | 'email'>;
// Esto no da error, simplemente omite 'email' si existe
type SinEmail = Omit<Usuario, 'email'>;
En resumen, Pick<T, K>
y Omit<T, K>
son herramientas esenciales en TypeScript que nos permiten crear tipos más específicos seleccionando o excluyendo propiedades. Estas utilidades nos ayudan a mantener nuestro código más seguro y expresivo, facilitando la creación de interfaces precisas para diferentes contextos de uso.
Readonly y Record
TypeScript ofrece varios tipos de utilidad que nos permiten modificar y adaptar tipos existentes para casos de uso específicos. Entre ellos, Readonly
y Record
son dos utilidades fundamentales que nos ayudan a trabajar con inmutabilidad y a crear mapas de datos tipados respectivamente.
Tipo Readonly
El tipo de utilidad Readonly<T>
convierte todas las propiedades de un tipo en propiedades de solo lectura, lo que significa que no pueden ser modificadas una vez que el objeto ha sido creado. Esto es especialmente útil para implementar patrones de inmutabilidad en nuestras aplicaciones.
La definición interna de Readonly
es bastante sencilla:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Veamos un ejemplo práctico:
interface Configuracion {
apiUrl: string;
timeout: number;
maxRetries: number;
}
// Creamos una configuración inmutable
const config: Readonly<Configuracion> = {
apiUrl: "https://api.ejemplo.com",
timeout: 3000,
maxRetries: 3
};
// Esto generaría un error de compilación
// config.timeout = 5000;
En este ejemplo, al aplicar Readonly<Configuracion>
, TypeScript nos impide modificar cualquier propiedad del objeto config
después de su inicialización, garantizando así su inmutabilidad.
Casos de uso comunes para Readonly
- Objetos de configuración: Para prevenir modificaciones accidentales en configuraciones críticas.
interface AppSettings {
theme: string;
language: string;
debugMode: boolean;
}
function initializeApp(settings: Readonly<AppSettings>) {
// Garantizamos que la configuración no será modificada dentro de la función
console.log(`Iniciando aplicación con tema: ${settings.theme}`);
// Esto generaría un error:
// settings.debugMode = false;
}
- Datos de referencia: Para valores constantes que no deben cambiar.
interface TasaImpuesto {
nombre: string;
porcentaje: number;
}
const IMPUESTOS: Readonly<TasaImpuesto[]> = [
{ nombre: "IVA Reducido", porcentaje: 10 },
{ nombre: "IVA General", porcentaje: 21 },
{ nombre: "IVA Superreducido", porcentaje: 4 }
];
// Esto generaría un error:
// IMPUESTOS[0].porcentaje = 11;
- Parámetros de funciones: Para garantizar que una función no modifique los objetos que recibe.
interface Usuario {
id: number;
nombre: string;
email: string;
}
function generarSaludo(usuario: Readonly<Usuario>): string {
// Garantizamos que no modificaremos el usuario
return `Hola, ${usuario.nombre}!`;
}
ReadonlyArray
Además de Readonly
, TypeScript proporciona ReadonlyArray<T>
, un tipo específico para arrays inmutables:
function procesarDatos(datos: ReadonlyArray<number>) {
// No podemos modificar el array
// datos.push(5); // Error
// datos[0] = 10; // Error
// Pero sí podemos leerlo
const suma = datos.reduce((a, b) => a + b, 0);
return suma;
}
const numeros = [1, 2, 3, 4];
procesarDatos(numeros); // Funciona correctamente
Tipo Record<K, T>
El tipo de utilidad Record<K, T>
crea un tipo que representa un objeto con claves de tipo K y valores de tipo T. Es extremadamente útil para crear mapas o diccionarios tipados.
La definición interna de Record
es:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Veamos un ejemplo básico:
// Creamos un mapa de IDs a usuarios
type UsuarioID = string;
interface DatosUsuario {
nombre: string;
edad: number;
activo: boolean;
}
const usuarios: Record<UsuarioID, DatosUsuario> = {
"usr_123": { nombre: "Ana", edad: 28, activo: true },
"usr_456": { nombre: "Carlos", edad: 34, activo: false },
"usr_789": { nombre: "Elena", edad: 25, activo: true }
};
// Acceso tipado
const usuario = usuarios["usr_123"];
console.log(usuario.nombre); // Ana
En este ejemplo, Record<UsuarioID, DatosUsuario>
crea un tipo que representa un objeto donde cada clave es un ID de usuario y cada valor es un objeto con la estructura de DatosUsuario
.
Casos de uso comunes para Record
- Mapas de entidades: Para almacenar colecciones de objetos indexados por ID.
interface Producto {
nombre: string;
precio: number;
stock: number;
}
// Catálogo de productos indexado por código SKU
const catalogo: Record<string, Producto> = {
"SKU-001": { nombre: "Teclado mecánico", precio: 89.99, stock: 45 },
"SKU-002": { nombre: "Ratón inalámbrico", precio: 29.99, stock: 28 },
"SKU-003": { nombre: "Monitor 27\"", precio: 249.99, stock: 12 }
};
// Fácil acceso por clave
function buscarProducto(sku: string): Producto | undefined {
return catalogo[sku];
}
- Tablas de búsqueda: Para mapear valores de un tipo a otro.
type CodigoPais = "ES" | "UK" | "FR" | "DE" | "IT";
// Mapa de códigos de país a nombres completos
const nombresPaises: Record<CodigoPais, string> = {
ES: "España",
UK: "Reino Unido",
FR: "Francia",
DE: "Alemania",
IT: "Italia"
};
function obtenerNombrePais(codigo: CodigoPais): string {
return nombresPaises[codigo];
}
console.log(obtenerNombrePais("ES")); // España
- Gestión de estado: Para organizar el estado en aplicaciones.
type EstadoComponente = "idle" | "loading" | "success" | "error";
interface MensajeEstado {
titulo: string;
descripcion: string;
}
// Definimos mensajes para cada estado posible
const mensajesEstado: Record<EstadoComponente, MensajeEstado> = {
idle: {
titulo: "Listo para cargar",
descripcion: "Pulse el botón para iniciar la carga de datos"
},
loading: {
titulo: "Cargando datos",
descripcion: "Por favor, espere mientras se cargan los datos"
},
success: {
titulo: "Carga completada",
descripcion: "Los datos se han cargado correctamente"
},
error: {
titulo: "Error de carga",
descripcion: "Ha ocurrido un error al cargar los datos"
}
};
function mostrarMensaje(estado: EstadoComponente) {
const mensaje = mensajesEstado[estado];
console.log(`${mensaje.titulo}: ${mensaje.descripcion}`);
}
Combinando Readonly y Record
Estas utilidades pueden combinarse para crear estructuras de datos inmutables más complejas:
// Definimos un mapa inmutable de configuraciones por entorno
type Entorno = "desarrollo" | "pruebas" | "produccion";
interface ConfiguracionEntorno {
apiUrl: string;
debugEnabled: boolean;
timeout: number;
}
// Mapa inmutable de configuraciones
const configuraciones: Readonly<Record<Entorno, ConfiguracionEntorno>> = {
desarrollo: {
apiUrl: "https://dev-api.ejemplo.com",
debugEnabled: true,
timeout: 5000
},
pruebas: {
apiUrl: "https://staging-api.ejemplo.com",
debugEnabled: true,
timeout: 3000
},
produccion: {
apiUrl: "https://api.ejemplo.com",
debugEnabled: false,
timeout: 2000
}
};
// No podemos modificar ni el mapa ni sus valores
// configuraciones.desarrollo.debugEnabled = false; // Error
// configuraciones.pruebas = { ... }; // Error
Record vs. objetos indexados
Es importante entender la diferencia entre Record
y los tipos de objetos indexados:
// Usando Record
type ConRecord = Record<string, number>;
// Usando tipo indexado
type ConIndexado = { [key: string]: number };
Aunque parecen similares, Record
ofrece algunas ventajas:
- Flexibilidad con las claves:
Record
permite usar tipos de unión para las claves. - Combinación con otros tipos de utilidad:
Record
se integra mejor con otras utilidades comoReadonly
. - Intención más clara:
Record
comunica mejor la intención de crear un mapa o diccionario.
// Solo acepta estas claves específicas
type Dia = "lunes" | "martes" | "miercoles" | "jueves" | "viernes";
type HorasTrabajadasPorDia = Record<Dia, number>;
const horasSemana: HorasTrabajadasPorDia = {
lunes: 8,
martes: 7.5,
miercoles: 8,
jueves: 6,
viernes: 5
// No podemos añadir "sabado" porque no está en el tipo Dia
};
Implementación personalizada
Para entender mejor cómo funcionan estas utilidades, podemos implementarlas nosotros mismos:
// Nuestra propia implementación de Readonly
type MiReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// Nuestra propia implementación de Record
type MiRecord<K extends keyof any, T> = {
[P in K]: T;
};
interface Persona {
nombre: string;
edad: number;
}
// Funcionarían igual que las utilidades nativas
type PersonaInmutable = MiReadonly<Persona>;
type MapaEdades = MiRecord<string, number>;
En resumen, Readonly<T>
y Record<K, T>
son herramientas fundamentales en TypeScript que nos permiten trabajar con inmutabilidad y crear mapas tipados respectivamente. Mientras que Readonly
nos ayuda a prevenir modificaciones accidentales en nuestros datos, Record
nos proporciona una forma elegante y tipada de crear diccionarios y mapas con claves y valores fuertemente tipados.
ReturnType y Parameters
TypeScript ofrece tipos de utilidad que nos permiten extraer información de tipos de funciones existentes. ReturnType
y Parameters
son dos utilidades especialmente útiles para trabajar con funciones de manera más segura y flexible, permitiéndonos acceder a los tipos de retorno y parámetros respectivamente.
Tipo ReturnType
El tipo de utilidad ReturnType<T>
extrae el tipo de retorno de una función. Esto nos permite reutilizar el tipo que devuelve una función sin tener que definirlo manualmente.
La definición interna de ReturnType
es:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Esta definición utiliza el operador infer
para inferir el tipo de retorno de la función.
Veamos un ejemplo práctico:
// Una función que devuelve un objeto con datos de usuario
function obtenerUsuario(id: number) {
return {
id,
nombre: "Usuario " + id,
fechaRegistro: new Date(),
activo: true
};
}
// Extraemos el tipo de retorno
type Usuario = ReturnType<typeof obtenerUsuario>;
// Ahora podemos usar este tipo en otras partes del código
function procesarUsuario(usuario: Usuario) {
console.log(`Procesando usuario: ${usuario.nombre}`);
// Tenemos acceso tipado a todas las propiedades
const diasRegistrado = Math.floor((Date.now() - usuario.fechaRegistro.getTime()) / (1000 * 60 * 60 * 24));
console.log(`Registrado hace ${diasRegistrado} días`);
}
En este ejemplo, ReturnType<typeof obtenerUsuario>
nos permite extraer automáticamente el tipo del objeto que devuelve la función obtenerUsuario
, sin necesidad de definir manualmente una interfaz Usuario
.
Casos de uso comunes para ReturnType
- Reutilización de tipos de API: Para trabajar con el tipo de respuesta de funciones de API.
async function obtenerDatosAPI() {
const respuesta = await fetch('https://api.ejemplo.com/datos');
return await respuesta.json();
}
// Extraemos el tipo de la respuesta para usarlo en otras partes
type DatosAPI = Awaited<ReturnType<typeof obtenerDatosAPI>>;
// Ahora podemos usar este tipo para funciones que procesan estos datos
function procesarDatos(datos: DatosAPI) {
// Procesamiento con tipo seguro
}
- Tipado de funciones de estado: Especialmente útil en gestores de estado como Redux.
function crearEstadoInicial() {
return {
usuario: null,
productos: [],
carrito: {
items: [],
total: 0
},
cargando: false,
error: null
};
}
// Extraemos el tipo de estado automáticamente
type Estado = ReturnType<typeof crearEstadoInicial>;
// Podemos usar este tipo para nuestros reductores
function reducer(estado: Estado, accion: any): Estado {
// Implementación del reductor
return estado;
}
- Captura de tipos de funciones de fábrica: Para trabajar con el resultado de funciones factory.
function crearServicio(config: { baseUrl: string }) {
return {
obtenerDatos: async () => {
// Implementación
return { datos: [] };
},
enviarDatos: async (datos: any) => {
// Implementación
return { éxito: true };
}
};
}
// Extraemos el tipo del servicio
type Servicio = ReturnType<typeof crearServicio>;
// Podemos usar este tipo para inyección de dependencias
function inicializarModulo(servicio: Servicio) {
// Uso tipado del servicio
servicio.obtenerDatos().then(resultado => {
console.log(resultado.datos);
});
}
Tipo Parameters
El tipo de utilidad Parameters<T>
extrae los tipos de los parámetros de una función como una tupla. Esto nos permite reutilizar los tipos de parámetros de una función existente.
La definición interna de Parameters
es:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
También utiliza el operador infer
para inferir los tipos de los parámetros.
Veamos un ejemplo:
// Una función con varios parámetros
function crearPedido(
cliente: { id: number; nombre: string },
productos: Array<{ id: string; cantidad: number }>,
direccionEntrega: string,
fechaEntrega?: Date
) {
// Implementación
return { id: Math.random().toString(36).substr(2, 9) };
}
// Extraemos el tipo de los parámetros
type ParametrosPedido = Parameters<typeof crearPedido>;
// Podemos acceder a parámetros individuales por índice
type Cliente = ParametrosPedido[0];
type Productos = ParametrosPedido[1];
// O podemos usar la tupla completa
function prepararPedido(...args: ParametrosPedido) {
return crearPedido(...args);
}
En este ejemplo, Parameters<typeof crearPedido>
nos da una tupla con los tipos de todos los parámetros de la función crearPedido
.
Casos de uso comunes para Parameters
- Decoradores de funciones: Para crear wrappers tipados que preservan los tipos originales.
function medirTiempo<F extends (...args: any[]) => any>(fn: F) {
return function(...args: Parameters<F>): ReturnType<F> {
const inicio = performance.now();
const resultado = fn(...args);
const fin = performance.now();
console.log(`Tiempo de ejecución: ${fin - inicio}ms`);
return resultado;
};
}
// Uso del decorador con tipos preservados
function calcularEstadisticas(numeros: number[], precision: number) {
// Cálculos complejos...
return { media: 0, mediana: 0, desviacion: 0 };
}
const calcularEstadisticasConMedicion = medirTiempo(calcularEstadisticas);
// Los tipos se preservan correctamente
const resultado = calcularEstadisticasConMedicion([1, 2, 3, 4, 5], 2);
- Memoización tipada: Para implementar caché de resultados preservando los tipos.
function memoizar<F extends (...args: any[]) => any>(fn: F) {
const cache = new Map<string, ReturnType<F>>();
return function(...args: Parameters<F>): ReturnType<F> {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
return cache.get(clave)!;
}
const resultado = fn(...args);
cache.set(clave, resultado);
return resultado;
};
}
// Función costosa que queremos memoizar
function calcularFactorial(n: number): number {
if (n <= 1) return 1;
return n * calcularFactorial(n - 1);
}
const factorialMemoizado = memoizar(calcularFactorial);
console.log(factorialMemoizado(10)); // Cálculo completo
console.log(factorialMemoizado(10)); // Resultado desde caché
- Proxies de funciones: Para interceptar llamadas a funciones manteniendo los tipos.
function registrarLlamadas<F extends (...args: any[]) => any>(fn: F, nombre: string) {
return function(...args: Parameters<F>): ReturnType<F> {
console.log(`Llamada a ${nombre} con argumentos:`, args);
try {
const resultado = fn(...args);
console.log(`${nombre} devolvió:`, resultado);
return resultado;
} catch (error) {
console.error(`${nombre} lanzó error:`, error);
throw error;
}
};
}
// Uso con una función de API
async function obtenerUsuarioPorId(id: number) {
// Implementación
return { id, nombre: "Usuario " + id };
}
const obtenerUsuarioConRegistro = registrarLlamadas(obtenerUsuarioPorId, "obtenerUsuarioPorId");
// Los tipos se mantienen
obtenerUsuarioConRegistro(123).then(usuario => console.log(usuario.nombre));
Acceso a parámetros específicos
Podemos acceder a parámetros individuales utilizando índices en la tupla devuelta por Parameters
:
function configurarAplicacion(
nombre: string,
opciones: {
tema: "claro" | "oscuro";
idioma: string;
notificaciones: boolean;
},
plugins?: string[]
) {
// Implementación
}
// Extraemos tipos específicos
type OpcionesApp = Parameters<typeof configurarAplicacion>[1];
type PluginsApp = Parameters<typeof configurarAplicacion>[2];
// Ahora podemos usar estos tipos de forma independiente
function guardarOpciones(opciones: OpcionesApp) {
localStorage.setItem("opciones", JSON.stringify(opciones));
}
function instalarPlugins(plugins: NonNullable<PluginsApp>) {
plugins.forEach(plugin => console.log(`Instalando ${plugin}...`));
}
Combinando ReturnType y Parameters
Estas utilidades son especialmente poderosas cuando se combinan entre sí o con otros tipos de utilidad:
// Definimos una función
function transformarDatos<T>(datos: T[], transformador: (item: T) => { valor: number; etiqueta: string }) {
return datos.map(transformador);
}
// Extraemos el tipo del transformador
type Transformador<T> = Parameters<typeof transformarDatos>[1];
// Extraemos el tipo de retorno del transformador
type ResultadoTransformacion<T> = ReturnType<Transformador<T>>;
// Extraemos el tipo del array resultante
type DatosTransformados<T> = ReturnType<typeof transformarDatos<T>>;
// Uso práctico
function crearGrafico<T>(datos: T[], transformador: Transformador<T>) {
const datosTransformados: DatosTransformados<T> = transformarDatos(datos, transformador);
// Renderizar gráfico con los datos transformados
console.log("Datos para gráfico:", datosTransformados);
}
Implementación con funciones genéricas
ReturnType
y Parameters
funcionan perfectamente con funciones genéricas:
// Función genérica
function convertir<T, U>(valor: T, conversor: (input: T) => U): U {
return conversor(valor);
}
// Extraer el tipo del segundo parámetro (el conversor)
type Conversor<T, U> = Parameters<typeof convertir<T, U>>[1];
// Crear un conversor específico
const stringANumero: Conversor<string, number> = (str) => parseFloat(str);
// Uso
const resultado = convertir("42.5", stringANumero);
console.log(resultado); // 42.5
Limitaciones y consideraciones
Es importante tener en cuenta algunas limitaciones:
- Funciones sobrecargadas: Con funciones sobrecargadas,
ReturnType
yParameters
extraen los tipos de la última sobrecarga.
function procesar(valor: string): string;
function procesar(valor: number): number;
function procesar(valor: string | number): string | number {
return typeof valor === "string" ? valor.toUpperCase() : valor * 2;
}
// Esto captura solo la última sobrecarga
type TipoRetorno = ReturnType<typeof procesar>; // string | number
- Funciones con genéricos: Para funciones con parámetros genéricos, necesitamos proporcionar los argumentos de tipo.
function identidad<T>(valor: T): T {
return valor;
}
// Necesitamos especificar el tipo genérico
type TipoString = ReturnType<typeof identidad<string>>; // string
En resumen, ReturnType<T>
y Parameters<T>
son herramientas poderosas que nos permiten extraer y reutilizar tipos de funciones existentes. Estas utilidades facilitan la creación de código más modular y tipado, permitiéndonos trabajar con funciones de manera más segura y flexible sin tener que duplicar definiciones de tipos.
Otros ejercicios de programación de TypeScript
Evalúa tus conocimientos de esta lección Tipos de utilidad (Partial, Required, Pick, etc) con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Funciones
Reto composición de funciones
Reto tipos especiales
Reto tipos genéricos
Módulos
Polimorfismo
Funciones TypeScript
Interfaces
Funciones puras
Reto namespaces
Funciones flecha
Polimorfismo
Operadores
Conversor de unidades
Funciones flecha
Control de flujo
Herencia
Clases
Proyecto validación de tipado
Clases y objetos
Encapsulación
Herencia
Proyecto sistema de votación
Reto genéricos con clases
Inmutabilidad
Interfaces
Funciones de alto orden
Reto map y filter
Control de flujo
Interfaces
Reto funciones orden superior
Herencia y clases abstractas
Reto tipos mapped
Herencia de clases
Reto funciones puras
Variables y constantes
Introducción a TypeScript
Reto testing unitario
Funciones de primera clase
Clases
OOP y CRUD en TypeScript
Interfaces y su implementación
Tipos genéricos
Namespaces
Operadores y expresiones
Proyecto generador de contraseñas
Reto unión e intersección
Encapsulación
Tipos de unión e intersección
Tipos de unión e intersección
Reto hola mundo en TS
Variables y constantes
Funciones puras
Control de flujo
Introducción a TypeScript
Resolución de módulos
Control de flujo
Reto tipos de utilidad
Reto tipos literales y condicionales
Reto exportar e importar
Propiedades y métodos
Tipos de utilidad
Clases y objetos
Tipos de datos, variables y constantes
Proyecto Minigestor de tareas
Operadores
Funciones flecha y contexto
Proyecto Inventario de productos
Funciones
Reto type aliases
Funciones de alto orden
Funciones y parámetros tipados
Tipos literales
Reto enums
Tipos de utilidad
Modificadores de acceso y encapsulación
Polimorfismo
Tipos genéricos
Reto módulos
Tipos literales
Inmutabilidad
Proyecto Generator de datos
Variables y constantes
Funciones de primera clase
Todas las lecciones de TypeScript
Accede a todas las lecciones de TypeScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Typescript
Introducción Y Entorno
Instalación Y Configuración De Typescript
Introducción Y Entorno
Tipos De Datos, Variables Y Constantes
Sintaxis
Operadores Y Expresiones
Sintaxis
Control De Flujo
Sintaxis
Funciones Y Parámetros Tipados
Sintaxis
Funciones Flecha Y Contexto
Sintaxis
Enums
Sintaxis
Type Aliases Y Aserciones De Tipo
Sintaxis
Clases Y Objetos
Programación Orientada A Objetos
Interfaces Y Su Implementación
Programación Orientada A Objetos
Modificadores De Acceso Y Encapsulación
Programación Orientada A Objetos
Herencia Y Clases Abstractas
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Decoradores Básicos
Programación Orientada A Objetos
Propiedades Y Métodos
Programación Orientada A Objetos
Inmutabilidad
Programación Funcional
Funciones Puras Y Efectos Secundarios
Programación Funcional
Funciones De Primera Clase
Programación Funcional
Funciones De Alto Orden
Programación Funcional
Conceptos Básicos E Inmutabilidad
Programación Funcional
Funciones De Primera Clase Y Orden Superior
Programación Funcional
Composición De Funciones
Programación Funcional
Métodos Funcionales De Arrays (Map, Filter, Reduce)
Programación Funcional
Tipos Literales Y Tipos Condicionales
Tipos Intermedios Y Avanzados
Tipos Genéricos Básicos
Tipos Intermedios Y Avanzados
Tipos De Unión E Intersección
Tipos Intermedios Y Avanzados
Tipos De Utilidad (Partial, Required, Pick, Etc)
Tipos Intermedios Y Avanzados
Unknown, Never Y Tipos Especiales
Tipos Intermedios Y Avanzados
Tipos Mapped
Tipos Intermedios Y Avanzados
Genéricos Con Clases E Interfaces
Tipos Intermedios Y Avanzados
Módulos
Namespaces Y Módulos
Namespaces
Namespaces Y Módulos
Resolución De Módulos
Namespaces Y Módulos
Exportación E Importación De Módulos
Namespaces Y Módulos
Introducción A Módulos
Namespaces Y Módulos
Testing Unitario En Typescript
Testing
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender y aplicar los tipos de utilidad Partial y Required para modificar la obligatoriedad de las propiedades de un tipo.
- Utilizar Pick y Omit para seleccionar o excluir propiedades específicas de un tipo y crear tipos derivados.
- Implementar Readonly y Record para trabajar con inmutabilidad y mapas tipados respectivamente.
- Extraer tipos de retorno y parámetros de funciones con ReturnType y Parameters para reutilizar tipos de funciones existentes.
- Combinar y personalizar tipos de utilidad para crear tipos complejos y adaptados a casos prácticos.