TypeScript aplicado en Ionic: tipado, interfaces y buenas prácticas

Capítulo 4

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

¿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.

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

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

  1. 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;}
  2. 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,  };}
  3. 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)

  1. Instala dependencias (si no están).

    npm i -D eslint prettier eslint-config-prettier eslint-plugin-prettier
  2. Crea/ajusta configuración de Prettier.

    // .prettierrc{  "singleQuote": true,  "semi": true,  "printWidth": 100,  "trailingComma": "all"}
  3. Integra Prettier con ESLint para evitar reglas en conflicto.

    // .eslintrc.json (ejemplo mínimo){  "extends": ["eslint:recommended", "plugin:prettier/recommended"],  "rules": {    "prettier/prettier": "error"  }}
  4. 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, Product y ApiResponse<T>.

  • Tipa el get con HttpClient.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 state y 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 profile como UserProfile | 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

ÁreaQué revisarEjemplo
ModelosInterfaces para DTO y dominioProductDto vs Product
HTTPRespuestas tipadas con genéricosget<ApiResponse<T>>()
Null safetyUsar ?., ?? y guardsisDefined
EstadosUniones o as const para estadostype ViewState
CalidadESLint + Prettier + scriptsnpm run lint

Ahora responde el ejercicio sobre el contenido:

En un servicio de Ionic (Angular) que consume un endpoint de productos, ¿qué enfoque mejora la seguridad de tipos y reduce el acoplamiento con el backend?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Tipar la respuesta con genéricos evita asumir estructuras inexistentes y el mapeo DTO → modelo desacopla la UI del contrato del backend. Al usar una función pura, la transformación es predecible y fácil de testear.

Siguiente capítulo

Gestión de estado y datos en Ionic con servicios y almacenamiento local

Arrow Right Icon
Portada de libro electrónico gratuitaIonic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript
40%

Ionic desde Cero: Crea Aplicaciones Híbridas con HTML, CSS y TypeScript

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.