¿Por qué TypeScript marca la diferencia en un proyecto Ionic?
En una app Ionic real, TypeScript te ayuda a detectar errores antes de ejecutar (por ejemplo, propiedades mal escritas, tipos incompatibles o valores null no contemplados). Además, mejora el autocompletado, facilita refactorizaciones y hace que el contrato entre componentes/servicios sea explícito.
Tipos en casos reales: primitivos, uniones y tipos avanzados
Primitivos y anotaciones útiles
En Ionic (Angular), es común tipar estados de UI, parámetros de métodos y valores de formularios. Evita any salvo casos muy justificados.
let isLoading: boolean = false;let pageTitle: string = 'Productos';let retryCount: number = 0;let lastSync: Date | null = null;Uniones (union types) para estados de UI
Un patrón práctico es modelar estados como un conjunto finito de valores.
type ViewState = 'idle' | 'loading' | 'success' | 'error';let state: ViewState = 'idle';function setState(next: ViewState) { state = next;}Tipos literales y as const para evitar strings “mágicos”
const ORDER_STATUS = { Pending: 'pending', Paid: 'paid', Cancelled: 'cancelled',} as const;type OrderStatus = typeof ORDER_STATUS[keyof typeof ORDER_STATUS];let status: OrderStatus = ORDER_STATUS.Pending;Intersecciones y utilidades (Partial, Pick, Omit)
Muy útil cuando tienes modelos base y variantes para edición o creación.
interface Product { id: string; name: string; price: number; description?: string;}type ProductCreate = Omit<Product, 'id'>;type ProductPatch = Partial<ProductCreate>;type ProductListItem = Pick<Product, 'id' | 'name' | 'price'>;Interfaces para modelos de datos (DTOs) y modelos de dominio
En apps reales conviene separar el “contrato de API” (DTO) del modelo que usa la UI. Esto reduce acoplamiento y te permite adaptar cambios del backend sin romper toda la app.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
Ejemplo: DTO de API vs modelo para la app
// Lo que llega del backend (DTO)interface ProductDto { id: string; title: string; price_cents: number; image_url?: string | null;}// Lo que usa la app (modelo)interface Product { id: string; name: string; price: number; imageUrl?: string;}Mapeo con función pura (recomendado)
Una función pura no modifica entradas ni depende de estado externo; es fácil de testear y reutilizar.
function mapProductDtoToProduct(dto: ProductDto): Product { return { id: dto.id, name: dto.title, price: dto.price_cents / 100, imageUrl: dto.image_url ?? undefined, };}Enums: cuándo usarlos y alternativas
Los enum pueden ser útiles para valores fijos (roles, tipos, categorías). En proyectos modernos también se usan objetos con as const (como arriba) para evitar ciertos inconvenientes de enum en runtime. Si decides usar enum, úsalo para representar un conjunto cerrado y documentado.
export enum UserRole { Admin = 'admin', Staff = 'staff', Customer = 'customer',}function canEditProducts(role: UserRole): boolean { return role === UserRole.Admin || role === UserRole.Staff;}Genéricos básicos: tipar utilidades y respuestas
Respuesta API genérica
Un patrón común es envolver respuestas en un contenedor con metadatos.
interface ApiResponse<T> { data: T; meta?: { requestId?: string; total?: number; }; error?: { code: string; message: string; };}Ejemplo con lista paginada
interface Paginated<T> { items: T[]; page: number; pageSize: number; total: number;}Función genérica para “normalizar” datos
function ensureArray<T>(value: T | T[] | null | undefined): T[] { if (value == null) return []; return Array.isArray(value) ? value : [value];}Tipado de respuestas HTTP en Ionic (Angular HttpClient)
Tipar las respuestas evita errores típicos como asumir propiedades que no existen o tratar números como strings. En Angular, HttpClient permite tipar el cuerpo con genéricos.
Paso a paso: servicio tipado con mapeo DTO → modelo
Define DTOs y modelos.
interface ProductDto { id: string; title: string; price_cents: number; image_url?: string | null;}interface Product { id: string; name: string; price: number; imageUrl?: string;}Crea un mapper (función pura).
function mapProductDtoToProduct(dto: ProductDto): Product { return { id: dto.id, name: dto.title, price: dto.price_cents / 100, imageUrl: dto.image_url ?? undefined, };}Tipa la respuesta del endpoint y transforma.
import { HttpClient } from '@angular/common/http';import { Injectable } from '@angular/core';import { map, Observable } from 'rxjs';@Injectable({ providedIn: 'root' })export class ProductsService { private readonly baseUrl = 'https://api.example.com'; constructor(private readonly http: HttpClient) {} getProducts(): Observable<Product[]> { return this.http.get<ApiResponse<ProductDto[]>>(`${this.baseUrl}/products`) .pipe(map(res => (res.data ?? []).map(mapProductDtoToProduct))); }}
Tipado de errores: evita asumir estructura
Los errores HTTP pueden variar. Tipar un “shape” mínimo y hacer narrowing es más seguro.
type ApiError = { code?: string; message?: string };function getErrorMessage(err: unknown): string { if (typeof err === 'object' && err !== null && 'message' in err) { const msg = (err as { message?: unknown }).message; return typeof msg === 'string' ? msg : 'Error desconocido'; } return 'Error desconocido';}Manejo de null / undefined sin sorpresas
Operador de encadenamiento opcional y coalescencia nula
interface UserProfile { name?: string | null; avatarUrl?: string | null;}function getDisplayName(profile: UserProfile | null | undefined): string { return profile?.name ?? 'Invitado';}Type guards (narrowing) para filtrar valores nulos
Muy útil al procesar listas provenientes de API o almacenamiento local.
function isDefined<T>(value: T | null | undefined): value is T { return value !== null && value !== undefined;}const urls = ['a.png', null, 'b.png', undefined].filter(isDefined);Evita ! (non-null assertion) como solución por defecto
! silencia el compilador, pero no evita fallos en runtime. Úsalo solo cuando tengas una garantía real (por ejemplo, después de una validación).
Buenas prácticas aplicadas: pureza, separación de responsabilidades y diseño de tipos
Funciones puras cuando sea posible
Ejemplo: cálculo de totales en un carrito. La función no debe mutar el arreglo ni depender de variables globales.
interface CartItem { productId: string; unitPrice: number; quantity: number;}function calcTotal(items: CartItem[]): number { return items.reduce((sum, it) => sum + it.unitPrice * it.quantity, 0);}Separación de responsabilidades: componente vs servicio vs mapper
Componente/página: orquesta UI (carga, error, render), no “parsea” DTOs ni arma URLs.
Servicio: comunicación HTTP y composición de observables/promesas.
Mappers: transformación DTO → modelo (funciones puras).
Tipos: en archivos dedicados (
models,dtos,types) para reutilización.
Evita any; prefiere unknown + validación
Cuando no controlas el origen de datos (por ejemplo, JSON.parse), usa unknown y valida.
function safeParseJson(value: string): unknown { try { return JSON.parse(value) as unknown; } catch { return null; }}Configuración de linting y formatting a nivel de proyecto
El objetivo es que el equipo escriba código consistente y que los errores comunes se detecten automáticamente.
Paso a paso: ESLint + Prettier (en un proyecto Ionic Angular)
Instala dependencias (si no están).
npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettierCrea/ajusta configuración de Prettier.
// .prettierrc{ "singleQuote": true, "semi": true, "printWidth": 100, "trailingComma": "all"}Integra Prettier con ESLint para evitar reglas en conflicto.
// .eslintrc.json (ejemplo mínimo){ "extends": ["eslint:recommended", "plugin:prettier/recommended"], "rules": { "prettier/prettier": "error" }}Añade scripts útiles.
// package.json{ "scripts": { "lint": "eslint . --ext .ts,.html", "format": "prettier --write ." }}
Si tu proyecto ya trae ESLint configurado por el tooling, mantén esa base y agrega Prettier como formateador, evitando duplicar reglas de estilo.
Ejercicios de refactorización: de JavaScript a TypeScript tipado
Ejercicio 1: tipar un servicio “rápido” con any
Antes (JavaScript/TypeScript sin tipado)
// products.service.tsgetProducts() { return this.http.get(this.baseUrl + '/products');}Tarea
Crea
ProductDto,ProductyApiResponse<T>.Tipa el
getconHttpClient.get<ApiResponse<ProductDto[]>>.Mapea a
Product[]con una función pura.
Pista
getProducts(): Observable<Product[]> { return this.http.get<ApiResponse<ProductDto[]>>(...).pipe( map(res => (res.data ?? []).map(mapProductDtoToProduct)), );}Ejercicio 2: eliminar “strings mágicos” con un tipo unión
Antes
let state = 'loading';function render() { if (state === 'sucess') { // typo silencioso }}Tarea
Define
type ViewState = 'idle' | 'loading' | 'success' | 'error'.Tipa
statey corrige el typo (el compilador debe avisarte).
Ejercicio 3: manejar null correctamente en datos de perfil
Antes
function getAvatar(profile) { return profile.avatarUrl.toLowerCase();}Tarea
Tipa
profilecomoUserProfile | null | undefined.Usa
?.y??para devolver una URL por defecto si no existe.
Posible solución
interface UserProfile { avatarUrl?: string | null;}function getAvatar(profile: UserProfile | null | undefined): string { return (profile?.avatarUrl ?? 'https://example.com/default.png').toLowerCase();}Ejercicio 4: convertir una función impura en pura
Antes
let total = 0;function addToTotal(price, qty) { total += price * qty; return total;}Tarea
Elimina el estado global.
Tipa parámetros y retorno.
Haz que la función dependa solo de sus entradas.
Posible solución
function addLine(total: number, price: number, qty: number): number { return total + price * qty;}Ejercicio 5: genéricos para reutilizar una utilidad
Antes
function first(items) { return items[0];}Tarea
Tipa la función con genéricos para que preserve el tipo del arreglo.
Contempla arreglo vacío devolviendo
undefined.
Posible solución
function first<T>(items: T[]): T | undefined { return items[0];}Checklist rápida para aplicar en tu proyecto Ionic
| Área | Qué revisar | Ejemplo |
|---|---|---|
| Modelos | Interfaces para DTO y dominio | ProductDto vs Product |
| HTTP | Respuestas tipadas con genéricos | get<ApiResponse<T>>() |
| Null safety | Usar ?., ?? y guards | isDefined |
| Estados | Uniones o as const para estados | type ViewState |
| Calidad | ESLint + Prettier + scripts | npm run lint |