Mejores Prácticas de TypeScript 2026

Mejores Prácticas de TypeScript 2026

Las mejores prácticas y patrones de TypeScript para escribir código limpio y mantenible.

16 de marzo de 20269 min de lectura

Introducción: TypeScript como Estándar de la Industria

En 2026, TypeScript se ha consolidado como el estándar de facto para el desarrollo de aplicaciones JavaScript a gran escala. Con más del 80% de los nuevos proyectos adoptando TypeScript desde el inicio, dominar sus mejores prácticas es esencial para cualquier desarrollador web moderno.

TypeScript no solo añade tipos estáticos a JavaScript, sino que proporciona un sistema de tipos increíblemente poderoso que, cuando se usa correctamente, puede prevenir categorías enteras de bugs antes de que lleguen a producción. Sin embargo, muchos desarrolladores solo aprovechan una fracción de su potencial.

Esta guía presenta las mejores prácticas de TypeScript actualizadas para 2026, incluyendo los últimos patrones, convenciones y técnicas que los equipos más productivos están utilizando en proyectos reales.

Configuración del Proyecto

tsconfig.json Estricto

La base de un proyecto TypeScript sólido comienza con una configuración estricta:

{
  "compilerOptions": {
    "target": "ES2024",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Las opciones clave que muchos desarrolladores pasan por alto:

  • noUncheckedIndexedAccess: Añade undefined al tipo de retorno al acceder a propiedades con índice, previniendo errores de acceso a undefined.
  • exactOptionalPropertyTypes: Distingue entre una propiedad opcional y una que puede ser undefined.
  • noPropertyAccessFromIndexSignature: Obliga a usar notación de corchetes para propiedades con index signatures.

Tipado Efectivo

Prefiere Interfaces para Objetos

// ✅ Bueno: Interfaces para objetos
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

// ✅ Bueno: Type aliases para uniones y tipos utilitarios
type Status = 'active' | 'inactive' | 'suspended';
type UserWithStatus = User & { status: Status };

// ❌ Evita: Type alias para objetos simples (las interfaces son más extensibles)
type UserType = {
  id: string;
  name: string;
};

Las interfaces son preferibles para objetos porque:

  1. Son más fáciles de extender con extends
  2. Proporcionan mejores mensajes de error
  3. Pueden ser mergeadas mediante declaration merging
  4. Tienen mejor rendimiento en el compilador

Usa Tipos Literales y Uniones Discriminadas

Las uniones discriminadas son uno de los patrones más poderosos de TypeScript:

// Unión discriminada para estados de una petición
interface LoadingState {
  status: 'loading';
}

interface SuccessState<T> {
  status: 'success';
  data: T;
}

interface ErrorState {
  status: 'error';
  error: string;
  code: number;
}

type RequestState<T> = LoadingState | SuccessState<T> | ErrorState;

// El compilador garantiza que manejas todos los casos
function handleRequest<T>(state: RequestState<T>): string {
  switch (state.status) {
    case 'loading':
      return 'Cargando...';
    case 'success':
      return `Datos: ${JSON.stringify(state.data)}`;
    case 'error':
      return `Error ${state.code}: ${state.error}`;
  }
}

Evita el Tipo any

El tipo any desactiva la verificación de tipos y elimina todas las ventajas de TypeScript:

// ❌ Malo: Usar any
function processData(data: any): any {
  return data.map((item: any) => item.value);
}

// ✅ Bueno: Usar tipos genéricos
function processData<T extends { value: unknown }>(data: T[]): T['value'][] {
  return data.map((item) => item.value);
}

// ✅ Bueno: Usar unknown cuando no conoces el tipo
function parseJSON(json: string): unknown {
  return JSON.parse(json);
}

// ✅ Bueno: Type guards para narrowing de unknown
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value &&
    'email' in value
  );
}

Genéricos Avanzados

Restricciones con Genéricos

// Función genérica con restricción
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Genérico con valor por defecto
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

// Genéricos condicionales
type IsArray<T> = T extends Array<infer U> ? U : never;

type ElementType = IsArray<string[]>; // string
type NeverType = IsArray<number>;     // never

Tipos Mapeados y Template Literals

// Tipo mapeado para hacer todas las propiedades opcionales y nullable
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Template literal types para rutas de API
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = '/users' | '/products' | '/orders';
type ApiEndpoint = `${HttpMethod} ${ApiPath}`;
// Resultado: "GET /users" | "GET /products" | ... (12 combinaciones)

// Tipo utilitario para crear getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }

Patrones y Buenas Prácticas

Manejo de Errores con Result Type

En lugar de lanzar excepciones, usa un tipo Result que hace explícito que una función puede fallar:

type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      return {
        success: false,
        error: new Error(`HTTP ${response.status}: ${response.statusText}`),
      };
    }

    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error ? error : new Error('Error desconocido'),
    };
  }
}

// Uso: el compilador te obliga a manejar el error
const result = await fetchUser('123');
if (result.success) {
  console.log(result.data.name); // TypeScript sabe que data existe
} else {
  console.error(result.error.message); // TypeScript sabe que error existe
}

Branded Types (Tipos Nominales)

TypeScript usa tipado estructural, pero a veces necesitas tipado nominal para evitar confusiones:

// Definir branded types
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

// Funciones constructoras
function createUserId(id: string): UserId {
  // Validación aquí
  return id as UserId;
}

function createOrderId(id: string): OrderId {
  return id as OrderId;
}

// Ahora no puedes mezclar IDs accidentalmente
function getUser(id: UserId): Promise<User> {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

const userId = createUserId('usr_123');
const orderId = createOrderId('ord_456');

getUser(userId);   // ✅ Correcto
// getUser(orderId); // ❌ Error de compilación

Validación de Datos en Runtime con Zod

TypeScript solo verifica tipos en tiempo de compilación. Para datos externos, necesitas validación en runtime:

import { z } from 'zod';

// Definir esquema de validación
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(['admin', 'user', 'moderator']),
  preferences: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    language: z.string().default('es'),
  }).optional(),
});

// Inferir el tipo TypeScript del esquema
type User = z.infer<typeof UserSchema>;

// Validar datos externos
function processUserData(data: unknown): User {
  return UserSchema.parse(data); // Lanza error si no es válido
}

// O usar safeParse para manejo seguro de errores
function safeProcessUser(data: unknown): Result<User> {
  const result = UserSchema.safeParse(data);
  if (result.success) {
    return { success: true, data: result.data };
  }
  return {
    success: false,
    error: new Error(result.error.issues.map(i => i.message).join(', ')),
  };
}

Inmutabilidad

Favorece la inmutabilidad para prevenir bugs sutiles:

// Usar readonly para propiedades
interface Config {
  readonly apiUrl: string;
  readonly apiKey: string;
  readonly maxRetries: number;
}

// Usar ReadonlyArray
function processItems(items: ReadonlyArray<string>): string[] {
  // items.push('nuevo'); // ❌ Error de compilación
  return [...items, 'nuevo']; // ✅ Crear nuevo array
}

// Usar as const para objetos literales
const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  PRODUCTS: '/products',
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];
// Tipo: '/' | '/about' | '/products'

// Usar Readonly recursivo para objetos profundos
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

Patrones para Funciones

Sobrecarga de Funciones

// Sobrecargas para diferentes tipos de entrada/salida
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: 'canvas'): HTMLCanvasElement;
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const input = createElement('input'); // Tipo: HTMLInputElement
const canvas = createElement('canvas'); // Tipo: HTMLCanvasElement

Parámetros con Builder Pattern

interface QueryBuilder<T> {
  where(condition: Partial<T>): QueryBuilder<T>;
  orderBy(field: keyof T, direction?: 'asc' | 'desc'): QueryBuilder<T>;
  limit(count: number): QueryBuilder<T>;
  execute(): Promise<T[]>;
}

function createQuery<T>(table: string): QueryBuilder<T> {
  const conditions: Partial<T>[] = [];
  let orderField: keyof T | undefined;
  let orderDir: 'asc' | 'desc' = 'asc';
  let limitCount: number | undefined;

  const builder: QueryBuilder<T> = {
    where(condition) {
      conditions.push(condition);
      return builder;
    },
    orderBy(field, direction = 'asc') {
      orderField = field;
      orderDir = direction;
      return builder;
    },
    limit(count) {
      limitCount = count;
      return builder;
    },
    async execute() {
      // Implementación de la consulta
      return [] as T[];
    },
  };

  return builder;
}

// Uso con autocompletado completo
const users = await createQuery<User>('users')
  .where({ status: 'active' })
  .orderBy('createdAt', 'desc')
  .limit(10)
  .execute();

Herramientas Complementarias

Para validar y formatear datos mientras trabajas con TypeScript, te recomendamos usar nuestro formateador de JSON para visualizar las estructuras de datos que definen tus tipos. El probador de regex es perfecto para validar las expresiones regulares que uses en tus validaciones de Zod o TypeScript nativo.

Consejos Finales

  1. Activa el modo estricto desde el inicio: Es mucho más difícil añadirlo a un proyecto existente.
  2. Evita type assertions (as): Cada as es un punto potencial de fallo. Usa type guards en su lugar.
  3. Aprovecha la inferencia: No anotes tipos donde TypeScript puede inferirlos correctamente.
  4. Documenta tipos complejos: Usa JSDoc para explicar tipos que no son obvios.
  5. Usa tipos utilitarios built-in: Partial, Required, Pick, Omit, Record son extremadamente útiles.
  6. Mantén tus tipos cerca del código: Coloca las interfaces junto al código que las utiliza.

Conclusión

TypeScript en 2026 ofrece un sistema de tipos extraordinariamente potente que, cuando se domina, transforma la forma en que escribes y mantienes código. Las prácticas presentadas en esta guía, desde la configuración estricta del compilador hasta patrones avanzados como branded types y uniones discriminadas, representan el estado del arte en desarrollo TypeScript.

La clave está en adoptar estas prácticas gradualmente, comenzando por la configuración estricta y el uso correcto de tipos básicos, y avanzando hacia genéricos y patrones más sofisticados a medida que ganas confianza. El esfuerzo invertido en aprender estas técnicas se traduce directamente en menos bugs, mejor mantenibilidad y mayor productividad a largo plazo.

Publicaciones relacionadas