TypeScript

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ícate

Partial 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 propiedades
  • K: 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 original
  • K: 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 usa K 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:

  1. Flexibilidad con las claves: Record permite usar tipos de unión para las claves.
  2. Combinación con otros tipos de utilidad: Record se integra mejor con otras utilidades como Readonly.
  3. 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 y Parameters 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.

Aprende TypeScript online

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

TypeScript
Test

Reto composición de funciones

TypeScript
Código

Reto tipos especiales

TypeScript
Código

Reto tipos genéricos

TypeScript
Código

Módulos

TypeScript
Test

Polimorfismo

TypeScript
Código

Funciones TypeScript

TypeScript
Código

Interfaces

TypeScript
Puzzle

Funciones puras

TypeScript
Puzzle

Reto namespaces

TypeScript
Código

Funciones flecha

TypeScript
Puzzle

Polimorfismo

TypeScript
Test

Operadores

TypeScript
Test

Conversor de unidades

TypeScript
Proyecto

Funciones flecha

TypeScript
Test

Control de flujo

TypeScript
Código

Herencia

TypeScript
Puzzle

Clases

TypeScript
Puzzle

Proyecto validación de tipado

TypeScript
Proyecto

Clases y objetos

TypeScript
Código

Encapsulación

TypeScript
Test

Herencia

TypeScript
Test

Proyecto sistema de votación

TypeScript
Proyecto

Reto genéricos con clases

TypeScript
Código

Inmutabilidad

TypeScript
Puzzle

Interfaces

TypeScript
Test

Funciones de alto orden

TypeScript
Test

Reto map y filter

TypeScript
Código

Control de flujo

TypeScript
Test

Interfaces

TypeScript
Código

Reto funciones orden superior

TypeScript
Código

Herencia y clases abstractas

TypeScript
Código

Reto tipos mapped

TypeScript
Código

Herencia de clases

TypeScript
Código

Reto funciones puras

TypeScript
Código

Variables y constantes

TypeScript
Puzzle

Introducción a TypeScript

TypeScript
Test

Reto testing unitario

TypeScript
Código

Funciones de primera clase

TypeScript
Puzzle

Clases

TypeScript
Test

OOP y CRUD en TypeScript

TypeScript
Proyecto

Interfaces y su implementación

TypeScript
Código

Tipos genéricos

TypeScript
Test

Namespaces

TypeScript
Test

Operadores y expresiones

TypeScript
Código

Proyecto generador de contraseñas

TypeScript
Proyecto

Reto unión e intersección

TypeScript
Código

Encapsulación

TypeScript
Puzzle

Tipos de unión e intersección

TypeScript
Test

Tipos de unión e intersección

TypeScript
Puzzle

Reto hola mundo en TS

TypeScript
Código

Variables y constantes

TypeScript
Código

Funciones puras

TypeScript
Test

Control de flujo

TypeScript
Código

Introducción a TypeScript

TypeScript
Código

Resolución de módulos

TypeScript
Test

Control de flujo

TypeScript
Puzzle

Reto tipos de utilidad

TypeScript
Código

Reto tipos literales y condicionales

TypeScript
Código

Reto exportar e importar

TypeScript
Código

Propiedades y métodos

TypeScript
Código

Tipos de utilidad

TypeScript
Test

Clases y objetos

TypeScript
Código

Tipos de datos, variables y constantes

TypeScript
Código

Proyecto Minigestor de tareas

TypeScript
Proyecto

Operadores

TypeScript
Puzzle

Funciones flecha y contexto

TypeScript
Código

Proyecto Inventario de productos

TypeScript
Proyecto

Funciones

TypeScript
Puzzle

Reto type aliases

TypeScript
Código

Funciones de alto orden

TypeScript
Puzzle

Funciones y parámetros tipados

TypeScript
Código

Tipos literales

TypeScript
Puzzle

Reto enums

TypeScript
Código

Tipos de utilidad

TypeScript
Puzzle

Modificadores de acceso y encapsulación

TypeScript
Código

Polimorfismo

TypeScript
Puzzle

Tipos genéricos

TypeScript
Puzzle

Reto módulos

TypeScript
Código

Tipos literales

TypeScript
Test

Inmutabilidad

TypeScript
Test

Proyecto Generator de datos

TypeScript
Proyecto

Variables y constantes

TypeScript
Test

Funciones de primera clase

TypeScript
Test

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

TypeScript

Introducción Y Entorno

Instalación Y Configuración De Typescript

TypeScript

Introducción Y Entorno

Tipos De Datos, Variables Y Constantes

TypeScript

Sintaxis

Operadores Y Expresiones

TypeScript

Sintaxis

Control De Flujo

TypeScript

Sintaxis

Funciones Y Parámetros Tipados

TypeScript

Sintaxis

Funciones Flecha Y Contexto

TypeScript

Sintaxis

Enums

TypeScript

Sintaxis

Type Aliases Y Aserciones De Tipo

TypeScript

Sintaxis

Clases Y Objetos

TypeScript

Programación Orientada A Objetos

Interfaces Y Su Implementación

TypeScript

Programación Orientada A Objetos

Modificadores De Acceso Y Encapsulación

TypeScript

Programación Orientada A Objetos

Herencia Y Clases Abstractas

TypeScript

Programación Orientada A Objetos

Polimorfismo

TypeScript

Programación Orientada A Objetos

Decoradores Básicos

TypeScript

Programación Orientada A Objetos

Propiedades Y Métodos

TypeScript

Programación Orientada A Objetos

Inmutabilidad

TypeScript

Programación Funcional

Funciones Puras Y Efectos Secundarios

TypeScript

Programación Funcional

Funciones De Primera Clase

TypeScript

Programación Funcional

Funciones De Alto Orden

TypeScript

Programación Funcional

Conceptos Básicos E Inmutabilidad

TypeScript

Programación Funcional

Funciones De Primera Clase Y Orden Superior

TypeScript

Programación Funcional

Composición De Funciones

TypeScript

Programación Funcional

Métodos Funcionales De Arrays (Map, Filter, Reduce)

TypeScript

Programación Funcional

Tipos Literales Y Tipos Condicionales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Genéricos Básicos

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Unión E Intersección

TypeScript

Tipos Intermedios Y Avanzados

Tipos De Utilidad (Partial, Required, Pick, Etc)

TypeScript

Tipos Intermedios Y Avanzados

Unknown, Never Y Tipos Especiales

TypeScript

Tipos Intermedios Y Avanzados

Tipos Mapped

TypeScript

Tipos Intermedios Y Avanzados

Genéricos Con Clases E Interfaces

TypeScript

Tipos Intermedios Y Avanzados

Módulos

TypeScript

Namespaces Y Módulos

Namespaces

TypeScript

Namespaces Y Módulos

Resolución De Módulos

TypeScript

Namespaces Y Módulos

Exportación E Importación De Módulos

TypeScript

Namespaces Y Módulos

Introducción A Módulos

TypeScript

Namespaces Y Módulos

Testing Unitario En Typescript

TypeScript

Testing

Accede GRATIS a TypeScript y certifícate

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.