TypeScript: tipos avanzados
Contenido
- Introducción
- Conocimiento previo
- Referencias
- Índice
- 1. Uniones de tipos
- 2. Intersecciones de tipos
- 3. Uniones discriminadas
- 4. Estrechamiento con funciones de guarda
- 5. Genéricos
- 6. keyof y typeof
- 7. Tipos utilitarios
- 8. Tipar respuestas de API
- 9. Importar y exportar tipos
- 10. Aserciones de tipo y operador satisfies
- 11. Clases y enums
- 12. Límites del tipado en tiempo de ejecución
- 13. Buenas prácticas
- 14. Ejercicios
Tutorial de continuación sobre TypeScript para desarrollo web en entorno cliente: uniones discriminadas, intersecciones, genéricos, keyof, typeof, tipos utilitarios, aserciones, satisfies, clases, enums, tipado de APIs y buenas prácticas para modelar datos complejos.
Introducción
Una vez conocidos los fundamentos de TypeScript, el siguiente paso es aprender a modelar datos más expresivos. En aplicaciones reales no basta con indicar que una variable es string o number: también necesitamos representar estados, respuestas de APIs, formularios, errores, colecciones y transformaciones de datos.
Este tutorial amplía el anterior con herramientas de tipado que aparecen con frecuencia en proyectos frontend modernos.
Conocimiento previo
Referencias
- TypeScript Handbook - Everyday Types
- TypeScript Handbook - Narrowing
- TypeScript Handbook - Generics
- TypeScript Handbook - Utility Types
Índice
- Uniones de tipos
- Intersecciones de tipos
- Uniones discriminadas
- Estrechamiento con funciones de guarda
- Genéricos
- keyof y typeof
- Tipos utilitarios
- Tipar respuestas de API
- Importar y exportar tipos
- Aserciones de tipo y operador satisfies
- Clases y enums
- Límites del tipado en tiempo de ejecución
- Buenas prácticas
- Ejercicios
TODO: añadir imagen con mapa de recursos avanzados de TypeScript: uniones, genéricos, utilitarios y validación de APIs.
1. Uniones de tipos
Una unión permite indicar que un valor puede tener varios tipos posibles.
type Identificador = number | string;
function mostrarIdentificador(id: Identificador): string { return `ID: ${id}`;}También se usan mucho para representar conjuntos cerrados de valores.
type EstadoPedido = 'pendiente' | 'pagado' | 'enviado' | 'cancelado';
function puedeEnviarse(estado: EstadoPedido): boolean { return estado === 'pagado';}Este enfoque evita errores de texto:
const estado: EstadoPedido = 'pagado';Si escribes un valor no contemplado, TypeScript avisará.
2. Intersecciones de tipos
Una intersección combina varios tipos en uno solo.
type ConId = { id: number;};
type ConFechas = { creadoEn: Date; actualizadoEn: Date;};
type ProductoPersistido = ConId & ConFechas & { nombre: string; precio: number;};El resultado debe cumplir todas las partes:
const producto: ProductoPersistido = { id: 1, creadoEn: new Date(), actualizadoEn: new Date(), nombre: 'Teclado', precio: 49.99,};Las intersecciones son útiles cuando varias entidades comparten rasgos comunes.
3. Uniones discriminadas
Una unión discriminada usa una propiedad común para distinguir variantes.
type ResultadoCarga = | { estado: 'cargando' } | { estado: 'error'; mensaje: string } | { estado: 'ok'; datos: string[] };Al comprobar estado, TypeScript sabe qué propiedades existen en cada caso.
function mostrarResultado(resultado: ResultadoCarga): string { switch (resultado.estado) { case 'cargando': return 'Cargando...'; case 'error': return `Error: ${resultado.mensaje}`; case 'ok': return `Elementos: ${resultado.datos.length}`; }}Este patrón es muy útil para estados de interfaz, respuestas de servidor y flujos de formularios.
4. Estrechamiento con funciones de guarda
El estrechamiento de tipos permite pasar de un tipo amplio a uno más concreto.
function esTexto(valor: unknown): valor is string { return typeof valor === 'string';}
function imprimirMayusculas(valor: unknown): void { if (esTexto(valor)) { console.log(valor.toUpperCase()); return; }
console.log('No es un texto');}La expresión valor is string indica a TypeScript que la función actúa como guarda de tipo.
También se puede usar in para comprobar propiedades:
type Usuario = { nombre: string; email: string };type Invitado = { nombre: string };
function tieneEmail(persona: Usuario | Invitado): persona is Usuario { return 'email' in persona;}5. Genéricos
Los genéricos permiten escribir funciones y tipos reutilizables sin perder información de tipo.
function envolverEnArray<T>(valor: T): T[] { return [valor];}
const numeros = envolverEnArray(5);const textos = envolverEnArray('hola');T representa un tipo que se decidirá al usar la función.
También se pueden usar genéricos en interfaces:
interface RespuestaApi<T> { ok: boolean; datos: T; error?: string;}
type Usuario = { id: number; nombre: string;};
const respuesta: RespuestaApi<Usuario[]> = { ok: true, datos: [ { id: 1, nombre: 'Ana' }, ],};Los genéricos son especialmente útiles en funciones de utilidad, componentes, hooks, servicios y respuestas de API.
6. keyof y typeof
keyof obtiene las claves de un tipo.
type Producto = { id: number; nombre: string; precio: number;};
type ClaveProducto = keyof Producto; // 'id' | 'nombre' | 'precio'Ejemplo práctico:
function obtenerPropiedad<T, K extends keyof T>(objeto: T, clave: K): T[K] { return objeto[clave];}
const producto = { id: 1, nombre: 'Ratón', precio: 20 };
const nombre = obtenerPropiedad(producto, 'nombre');typeof permite obtener el tipo de un valor existente.
const configuracion = { apiUrl: 'https://api.example.com', reintentos: 3,};
type Configuracion = typeof configuracion;Esto evita duplicar manualmente tipos cuando ya existe un objeto que sirve de referencia.
7. Tipos utilitarios
TypeScript incluye tipos utilitarios para transformar tipos existentes.
interface Usuario { id: number; nombre: string; email: string; activo: boolean;}
type UsuarioEditable = Partial<Usuario>;type UsuarioSinEmail = Omit<Usuario, 'email'>;type DatosContacto = Pick<Usuario, 'nombre' | 'email'>;type UsuarioSoloLectura = Readonly<Usuario>;Usos habituales:
| Utilitario | Uso |
|---|---|
Partial<T> | Hace opcionales todas las propiedades. |
Required<T> | Hace obligatorias todas las propiedades. |
Pick<T, K> | Selecciona algunas propiedades. |
Omit<T, K> | Excluye algunas propiedades. |
Readonly<T> | Impide modificar propiedades. |
Record<K, T> | Crea un objeto con claves y valores tipados. |
Ejemplo con Record:
type Rol = 'admin' | 'editor' | 'lector';
const permisos: Record<Rol, string[]> = { admin: ['crear', 'editar', 'eliminar'], editor: ['crear', 'editar'], lector: ['leer'],};8. Tipar respuestas de API
Cuando consumimos una API, conviene definir qué forma esperamos recibir.
interface UsuarioApi { id: number; name: string; email: string;}
async function cargarUsuarios(): Promise<UsuarioApi[]> { const response = await fetch('https://jsonplaceholder.typicode.com/users');
if (!response.ok) { throw new Error(`Error HTTP: ${response.status}`); }
const usuarios = await response.json() as UsuarioApi[]; return usuarios;}La anotación as UsuarioApi[] indica a TypeScript qué forma esperamos, pero no valida los datos en tiempo de ejecución. Si la API puede devolver datos desconocidos o no confiables, necesitarás validación adicional.
Una función que consume esa API puede beneficiarse del tipo:
async function mostrarEmails(): Promise<void> { const usuarios = await cargarUsuarios();
for (const usuario of usuarios) { console.log(usuario.email); }}9. Importar y exportar tipos
En proyectos grandes, los tipos se reparten entre varios archivos.
Archivo types.ts:
export interface Producto { id: number; nombre: string; precio: number;}Archivo productos.ts:
import type { Producto } from './types';
export function calcularTotal(productos: Producto[]): number { return productos.reduce((total, producto) => total + producto.precio, 0);}import type deja claro que la importación solo se usa para tipado. Esto ayuda a separar tipos de código ejecutable.
10. Aserciones de tipo y operador satisfies
Una aserción de tipo indica a TypeScript que trate un valor como un tipo concreto. No transforma el valor en tiempo de ejecución; solo cambia la información de tipos durante el desarrollo.
const elemento = document.querySelector('#mensaje') as HTMLParagraphElement;elemento.textContent = 'Texto actualizado';Las aserciones deben usarse con cuidado. Si el selector no encuentra un párrafo, el código puede fallar en ejecución aunque TypeScript no avise.
Cuando quieres comprobar que un objeto cumple una forma sin perder la inferencia de sus valores concretos, puedes usar satisfies.
type Ruta = { titulo: string; url: string;};
const rutas = { inicio: { titulo: 'Inicio', url: '/' }, productos: { titulo: 'Productos', url: '/productos' },} satisfies Record<string, Ruta>;satisfies ayuda a validar la estructura esperada sin convertir todo el objeto a un tipo más amplio de lo necesario.
11. Clases y enums
TypeScript permite tipar clases de JavaScript. Aunque en frontend moderno se usan muchos objetos y funciones, las clases siguen apareciendo en algunos frameworks, servicios o modelos.
class Producto { constructor( public id: number, public nombre: string, public precio: number, ) {}
aplicarDescuento(porcentaje: number): number { return this.precio - this.precio * porcentaje; }}Los enums permiten definir un conjunto de constantes con nombre.
enum EstadoPedido { Pendiente = 'pendiente', Pagado = 'pagado', Enviado = 'enviado', Cancelado = 'cancelado',}
function puedeEnviarse(estado: EstadoPedido): boolean { return estado === EstadoPedido.Pagado;}En muchos casos, una unión de literales puede ser más simple que un enum:
type EstadoPedidoLiteral = 'pendiente' | 'pagado' | 'enviado' | 'cancelado';La decisión depende del proyecto y del estilo del equipo. Para empezar, las uniones de literales suelen ser más fáciles de leer.
12. Límites del tipado en tiempo de ejecución
TypeScript revisa el código durante el desarrollo, pero los tipos no validan automáticamente los datos que llegan en ejecución. Una respuesta de API, un formulario o un archivo externo pueden no tener la forma esperada.
Este ejemplo compila, pero no garantiza que la API devuelva usuarios válidos:
const usuarios = await response.json() as UsuarioApi[];Si los datos son críticos, debes validar en tiempo de ejecución:
function esUsuarioApi(valor: unknown): valor is UsuarioApi { if (typeof valor !== 'object' || valor === null) { return false; }
return 'id' in valor && 'email' in valor;}La validación puede hacerse con funciones propias o con librerías específicas cuando el proyecto lo justifique. Lo importante es no confundir “TypeScript cree que esto tiene una forma” con “este dato ha sido comprobado en ejecución”.
13. Buenas prácticas
- Modela estados con uniones discriminadas cuando haya varias variantes claras.
- Usa genéricos cuando una función deba conservar el tipo recibido.
- Evita tipos demasiado complejos si una interfaz sencilla resuelve el problema.
- Usa
unknownpara datos externos hasta validarlos. - No uses
aspara esconder errores; úsalo solo cuando tengas una razón clara. - Usa
satisfiescuando quieras comprobar una estructura sin perder inferencia útil. - Valida datos externos en tiempo de ejecución si son importantes para la aplicación.
- Extrae tipos compartidos a archivos específicos cuando empiecen a repetirse.
- Prefiere tipos pequeños y componibles frente a tipos enormes difíciles de leer.
14. Ejercicios
Ejercicio 1: Definir un tipo para el estado de un formulario
Define un tipo EstadoFormulario con los estados inicial, enviando, error y enviado.
Ejercicio 2: Crear una unión discriminada
Crea una unión discriminada Resultado<T> para representar éxito o error.
Ejercicio 3: Escribir una función genérica
Escribe una función genérica primero<T>(elementos: T[]): T | undefined.
Ejercicio 4: Usar Pick para crear un tipo
Usa Pick para crear un tipo ResumenProducto con solo nombre y precio.
Ejercicio 5: Crear una función con keyof
Crea una función obtenerPropiedad usando keyof, como en el ejemplo del tutorial.
Ejercicio 6: Definir una interfaz para una respuesta de API
Define una interfaz para una respuesta de API y escribe una función async que devuelva Promise<T[]>.
Ejercicio 7: Usar satisfies
Crea un objeto de rutas de navegación y usa satisfies para comprobar que todas tienen titulo y url.
Ejercicio 8: Comparar enum y unión literal
Modela los estados de un pedido con un enum y después con una unión de literales. Explica qué opción te parece más clara en este caso.
Ejercicio 9: Validar datos externos
Crea una función de guarda que reciba unknown y compruebe si tiene la forma mínima de un producto con id, nombre y precio.
Estos recursos de tipado permiten construir aplicaciones más robustas sin perder la flexibilidad de JavaScript. La clave está en usarlos para expresar mejor el dominio del problema, no para complicar el código sin necesidad.