Skip to content

TypeScript desde 0 a Profesional (2026)

Tutorial

40 min read

1. ¿Qué es TypeScript y por qué usarlo?

TypeScript es un superconjunto tipado de JavaScript desarrollado y mantenido por Microsoft. Compila a JavaScript estándar y puede ejecutarse en cualquier entorno donde corra JS.

Ventajas clave en 2026

  • Detección de errores en tiempo de compilación — los bugs aparecen antes de llegar a producción
  • Autocompletado e IntelliSense — productividad muy superior en cualquier editor
  • Refactoring seguro — renombrar o mover código sin miedo
  • Documentación viva — los tipos sirven como documentación que siempre está actualizada
  • Ecosistema maduro — prácticamente todas las librerías populares tienen soporte de tipos

JavaScript vs TypeScript

// JavaScript — sin protección
function suma(a, b) {
  return a + b;
}
suma("10", 5); // "105" — bug silencioso
// TypeScript — error detectado al compilar
function suma(a: number, b: number): number {
  return a + b;
}
suma("10", 5); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'

2. Instalación y configuración

Requisitos previos

  • Node.js 20+ (LTS recomendado)
  • npm, pnpm o bun

Instalación global

npm install -g typescript
tsc --version # Verifica la versión instalada

Inicializar un proyecto

mkdir mi-proyecto-ts
cd mi-proyecto-ts
npm init -y
npm install -D typescript @types/node
npx tsc --init

tsconfig.json básico (2026)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "exactOptionalPropertyTypes": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Scripts en package.json

{
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "typecheck": "tsc --noEmit"
  }
}

Tip 2026: Usa tsx en lugar de ts-node para desarrollo — es mucho más rápido al estar basado en esbuild.


3. Tipos básicos

Primitivos

// Tipos primitivos
const nombre: string = "Ana García";
const edad: number = 28;
const activo: boolean = true;
const nulo: null = null;
const indefinido: undefined = undefined;
const simbolo: symbol = Symbol("id");
const grande: bigint = 9007199254740991n;

Arrays y Tuplas

// Arrays — dos sintaxis equivalentes
const numeros: number[] = [1, 2, 3];
const textos: Array<string> = ["hola", "mundo"];

// Array de solo lectura
const constantes: readonly number[] = [1, 2, 3];
// constantes.push(4); ❌ Error

// Tuplas — longitud y tipos fijos
const coordenada: [number, number] = [40.4168, -3.7038];
const entrada: [string, number] = ["España", 47_350_000];

// Tupla con elemento opcional y rest
type RGB = [red: number, green: number, blue: number, alpha?: number];
const rojo: RGB = [255, 0, 0];
const rojoTransparente: RGB = [255, 0, 0, 0.5];

any, unknown, never y void

// any — desactiva el sistema de tipos (evitar siempre que sea posible)
let cualquierCosa: any = "texto";
cualquierCosa = 42;
cualquierCosa.metodoInventado(); // No da error, pero puede fallar en runtime

// unknown — versión segura de any
let entrada: unknown = obtenerDatoExterno();
// entrada.toLowerCase(); ❌ Error — debes verificar el tipo primero
if (typeof entrada === "string") {
  console.log(entrada.toLowerCase()); // ✅ Ahora es seguro
}

// never — representa valores que nunca ocurren
function lanzarError(mensaje: string): never {
  throw new Error(mensaje);
}

function verificarExhaustividad(valor: never): never {
  throw new Error(`Caso no manejado: ${valor}`);
}

// void — funciones que no retornan valor significativo
function imprimirSaludo(): void {
  console.log("¡Hola!");
}

Enums

// Enum numérico
enum Direccion {
  Norte,  // 0
  Sur,    // 1
  Este,   // 2
  Oeste,  // 3
}

// Enum de cadena (recomendado en 2026)
enum Estado {
  Activo = "ACTIVO",
  Inactivo = "INACTIVO",
  Pendiente = "PENDIENTE",
}

const estadoUsuario: Estado = Estado.Activo;
console.log(estadoUsuario); // "ACTIVO"

// Const enums — se eliminan en compilación (mejor rendimiento)
const enum TipoHttp {
  Get = "GET",
  Post = "POST",
  Put = "PUT",
  Delete = "DELETE",
}

Alternativa moderna a enums: Muchos equipos prefieren usar as const con objetos:

const ESTADO = {
  Activo: "ACTIVO",
  Inactivo: "INACTIVO",
  Pendiente: "PENDIENTE",
} as const;

type Estado = typeof ESTADO[keyof typeof ESTADO];
// Estado = "ACTIVO" | "INACTIVO" | "PENDIENTE"

4. Funciones tipadas

Tipos de parámetros y retorno

// Parámetros tipados y tipo de retorno explícito
function saludar(nombre: string, titulo?: string): string {
  const t = titulo ? `${titulo} ` : "";
  return `Hola, ${t}${nombre}!`;
}

saludar("Carlos");           // "Hola, Carlos!"
saludar("Ana", "Dra.");      // "Hola, Dra. Ana!"

// Parámetros con valor por defecto
function crearUsuario(
  nombre: string,
  rol: string = "visitante",
  activo: boolean = true
): { nombre: string; rol: string; activo: boolean } {
  return { nombre, rol, activo };
}

// Parámetros rest
function sumarTodos(...numeros: number[]): number {
  return numeros.reduce((acc, n) => acc + n, 0);
}

sumarTodos(1, 2, 3, 4, 5); // 15

Tipos de función

// Definir el tipo de una función
type Operacion = (a: number, b: number) => number;

const sumar: Operacion = (a, b) => a + b;
const multiplicar: Operacion = (a, b) => a * b;

// Sobrecarga de funciones
function procesar(valor: string): string;
function procesar(valor: number): number;
function procesar(valor: string | number): string | number {
  if (typeof valor === "string") {
    return valor.toUpperCase();
  }
  return valor * 2;
}

const r1 = procesar("hola"); // tipo: string
const r2 = procesar(5);      // tipo: number

Funciones de orden superior

// Callbacks tipados
function filtrar<T>(array: T[], condicion: (item: T) => boolean): T[] {
  return array.filter(condicion);
}

const numeros = [1, 2, 3, 4, 5, 6];
const pares = filtrar(numeros, n => n % 2 === 0); // [2, 4, 6]

// Composición de funciones
const componer = <T>(...fns: Array<(x: T) => T>) =>
  (x: T): T =>
    fns.reduceRight((acc, fn) => fn(acc), x);

const procesar2 = componer(
  (s: string) => s.trim(),
  (s: string) => s.toLowerCase(),
  (s: string) => s.replace(/\s+/g, "-")
);

procesar2("  Hola Mundo  "); // "hola-mundo"

5. Interfaces y Types

Interface

interface Usuario {
  readonly id: number;       // Solo lectura
  nombre: string;
  email: string;
  edad?: number;             // Opcional
  avatarUrl?: string;
}

// Las interfaces se pueden extender
interface UsuarioAdmin extends Usuario {
  permisos: string[];
  nivelAcceso: 1 | 2 | 3;
}

// Las interfaces se pueden fusionar (declaration merging)
interface Ventana {
  ancho: number;
}
interface Ventana {
  alto: number;
}
// Ventana ahora tiene: ancho y alto

Type Alias

// Types son más flexibles que las interfaces
type ID = string | number;

type Coordenadas = {
  lat: number;
  lng: number;
};

// Los types soportan tipos primitivos, uniones e intersecciones
type StringONumber = string | number;
type AdminUsuario = Usuario & { esAdmin: boolean };

// Tipos literales
type Direccion = "norte" | "sur" | "este" | "oeste";
type CodigoHTTP = 200 | 201 | 400 | 401 | 403 | 404 | 500;

Interface vs Type — ¿Cuándo usar cada uno?

CaracterísticaInterfaceType
Declaration merging
Extends / implements❌ (usa &)
Tipos primitivos
Uniones / intersecciones
Tipos mapeados
Mejor para objetos/clases

Regla práctica 2026: Usa interface para contratos de objetos y clases. Usa type para uniones, alias de tipos primitivos y tipos calculados.


6. Clases y Programación Orientada a Objetos

Clases básicas

class Persona {
  // Modificadores de acceso
  readonly id: number;
  public nombre: string;
  protected edad: number;
  private #email: string; // Campo privado nativo (ES2022)

  constructor(id: number, nombre: string, edad: number, email: string) {
    this.id = id;
    this.nombre = nombre;
    this.edad = edad;
    this.#email = email;
  }

  // Getter y setter
  get email(): string {
    return this.#email;
  }

  set email(nuevoEmail: string) {
    if (!nuevoEmail.includes("@")) throw new Error("Email inválido");
    this.#email = nuevoEmail;
  }

  saludar(): string {
    return `Hola, soy ${this.nombre}`;
  }

  toString(): string {
    return `Persona(${this.id}, ${this.nombre})`;
  }
}

Herencia e interfaces

interface Serializable {
  serializar(): string;
  deserializar(data: string): void;
}

class Empleado extends Persona implements Serializable {
  constructor(
    id: number,
    nombre: string,
    edad: number,
    email: string,
    public readonly empresa: string,
    private salario: number
  ) {
    super(id, nombre, edad, email);
  }

  obtenerSalario(): number {
    return this.salario;
  }

  serializar(): string {
    return JSON.stringify({
      id: this.id,
      nombre: this.nombre,
      empresa: this.empresa,
    });
  }

  deserializar(data: string): void {
    // implementación
  }

  // Sobreescribir método del padre
  override saludar(): string {
    return `${super.saludar()}, trabajo en ${this.empresa}`;
  }
}

Clases abstractas

abstract class Figura {
  abstract calcularArea(): number;
  abstract calcularPerimetro(): number;

  // Método concreto disponible para todas las subclases
  describir(): string {
    return `Área: ${this.calcularArea()}, Perímetro: ${this.calcularPerimetro()}`;
  }
}

class Circulo extends Figura {
  constructor(private radio: number) {
    super();
  }

  calcularArea(): number {
    return Math.PI * this.radio ** 2;
  }

  calcularPerimetro(): number {
    return 2 * Math.PI * this.radio;
  }
}

class Rectangulo extends Figura {
  constructor(private ancho: number, private alto: number) {
    super();
  }

  calcularArea(): number {
    return this.ancho * this.alto;
  }

  calcularPerimetro(): number {
    return 2 * (this.ancho + this.alto);
  }
}

// Polimorfismo
const figuras: Figura[] = [new Circulo(5), new Rectangulo(4, 6)];
figuras.forEach(f => console.log(f.describir()));

Clases estáticas y Singleton

class Configuracion {
  private static instancia: Configuracion;
  private datos: Map<string, unknown> = new Map();

  private constructor() {}

  static obtenerInstancia(): Configuracion {
    if (!Configuracion.instancia) {
      Configuracion.instancia = new Configuracion();
    }
    return Configuracion.instancia;
  }

  establecer(clave: string, valor: unknown): void {
    this.datos.set(clave, valor);
  }

  obtener<T>(clave: string): T | undefined {
    return this.datos.get(clave) as T;
  }
}

const config = Configuracion.obtenerInstancia();
config.establecer("apiUrl", "https://api.ejemplo.com");

7. Genéricos

Los genéricos son una de las características más poderosas de TypeScript. Permiten crear componentes reutilizables que funcionan con cualquier tipo.

Funciones genéricas

// Sin genéricos — perdemos el tipo
function primerElemento(array: any[]): any {
  return array[0];
}

// Con genéricos — preservamos el tipo
function primerElemento<T>(array: T[]): T | undefined {
  return array[0];
}

const num = primerElemento([1, 2, 3]);    // tipo: number | undefined
const str = primerElemento(["a", "b"]);   // tipo: string | undefined

// Múltiples parámetros de tipo
function zip<A, B>(a: A[], b: B[]): [A, B][] {
  return a.map((item, i) => [item, b[i]]);
}

const pares = zip([1, 2, 3], ["a", "b", "c"]);
// tipo: [number, string][]

Restricciones en genéricos

// Restricción con extends
function obtenerPropiedad<T, K extends keyof T>(obj: T, clave: K): T[K] {
  return obj[clave];
}

const usuario = { nombre: "Ana", edad: 30, email: "ana@ejemplo.com" };
const nombre = obtenerPropiedad(usuario, "nombre"); // tipo: string
const edad = obtenerPropiedad(usuario, "edad");     // tipo: number
// obtenerPropiedad(usuario, "inventada");           // ❌ Error

// Restricción con interfaz
interface TieneId {
  id: number | string;
}

function buscarPorId<T extends TieneId>(items: T[], id: number | string): T | undefined {
  return items.find(item => item.id === id);
}

Clases e interfaces genéricas

// Clase genérica — Repositorio genérico
interface Entidad {
  id: number;
}

class Repositorio<T extends Entidad> {
  private items: T[] = [];

  agregar(item: T): void {
    this.items.push(item);
  }

  obtenerPorId(id: number): T | undefined {
    return this.items.find(i => i.id === id);
  }

  obtenerTodos(): T[] {
    return [...this.items];
  }

  eliminar(id: number): boolean {
    const indice = this.items.findIndex(i => i.id === id);
    if (indice === -1) return false;
    this.items.splice(indice, 1);
    return true;
  }

  actualizar(id: number, cambios: Partial<T>): T | undefined {
    const indice = this.items.findIndex(i => i.id === id);
    if (indice === -1) return undefined;
    this.items[indice] = { ...this.items[indice], ...cambios };
    return this.items[indice];
  }
}

interface Producto extends Entidad {
  nombre: string;
  precio: number;
  stock: number;
}

const repositorioProductos = new Repositorio<Producto>();
repositorioProductos.agregar({ id: 1, nombre: "Laptop", precio: 999, stock: 10 });
repositorioProductos.agregar({ id: 2, nombre: "Ratón", precio: 29, stock: 50 });

const laptop = repositorioProductos.obtenerPorId(1); // tipo: Producto | undefined

Tipos genéricos avanzados

// Promesa genérica
async function fetchDatos<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  return response.json() as Promise<T>;
}

interface ApiUsuario {
  id: number;
  name: string;
  email: string;
}

const usuario = await fetchDatos<ApiUsuario>("https://api.ejemplo.com/users/1");
// usuario.name — TypeScript sabe el tipo exacto

// Estado genérico (patrón común en React/stores)
type EstadoCarga<T> =
  | { estado: "cargando" }
  | { estado: "exito"; datos: T }
  | { estado: "error"; mensaje: string };

function renderizarEstado<T>(estado: EstadoCarga<T>): string {
  switch (estado.estado) {
    case "cargando":
      return "Cargando...";
    case "exito":
      return `Datos: ${JSON.stringify(estado.datos)}`;
    case "error":
      return `Error: ${estado.mensaje}`;
  }
}

8. Tipos avanzados

Union Types e Intersection Types

// Union — uno u otro
type Id = string | number;
type Respuesta = "si" | "no" | "tal vez";

// Intersection — combinar tipos
type AdminConPermisos = Usuario & {
  permisos: string[];
  ultimoAcceso: Date;
};

Type Guards (Guardias de tipo)

// typeof guard
function procesarValor(valor: string | number): string {
  if (typeof valor === "string") {
    return valor.toUpperCase();
  }
  return valor.toFixed(2);
}

// instanceof guard
class Perro {
  ladrar() { return "¡Guau!"; }
}
class Gato {
  maullar() { return "¡Miau!"; }
}

function hacerSonido(animal: Perro | Gato): string {
  if (animal instanceof Perro) {
    return animal.ladrar();
  }
  return animal.maullar();
}

// Custom type guard — el más poderoso
interface Cuadrado { tipo: "cuadrado"; lado: number }
interface Circulo { tipo: "circulo"; radio: number }

type Forma = Cuadrado | Circulo;

// El retorno `valor is Cuadrado` es el type predicate
function esCuadrado(forma: Forma): forma is Cuadrado {
  return forma.tipo === "cuadrado";
}

function calcularArea(forma: Forma): number {
  if (esCuadrado(forma)) {
    return forma.lado ** 2;  // TypeScript sabe que es Cuadrado
  }
  return Math.PI * forma.radio ** 2; // TypeScript sabe que es Circulo
}

// Discriminated unions — patrón muy usado
type Resultado<T, E = Error> =
  | { ok: true; valor: T }
  | { ok: false; error: E };

function dividir(a: number, b: number): Resultado<number, string> {
  if (b === 0) return { ok: false, error: "División por cero" };
  return { ok: true, valor: a / b };
}

const resultado = dividir(10, 2);
if (resultado.ok) {
  console.log(resultado.valor); // tipo: number
} else {
  console.error(resultado.error); // tipo: string
}

Tipos condicionales

// Tipo condicional básico
type EsString<T> = T extends string ? true : false;

type A = EsString<string>;  // true
type B = EsString<number>;  // false

// Inferencia con infer
type ObtenerRetorno<T> = T extends (...args: any[]) => infer R ? R : never;

type RetornoString = ObtenerRetorno<() => string>;  // string
type RetornoNumero = ObtenerRetorno<() => number>;  // number

// Tipos condicionales distribuidos
type NoNulo<T> = T extends null | undefined ? never : T;
type SoloStrings<T> = T extends string ? T : never;

type Limpio = NoNulo<string | null | undefined | number>; // string | number
type Textos = SoloStrings<string | number | boolean>;     // string

Tipos mapeados

// Crear variantes de tipos existentes
type OpcionalTodos<T> = {
  [K in keyof T]?: T[K];
};

type SoloLectura<T> = {
  readonly [K in keyof T]: T[K];
};

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // Quitar readonly
};

type Requerido<T> = {
  [K in keyof T]-?: T[K]; // Quitar opcionalidad
};

// Template literal types — muy potente
type EventosCRUD<T extends string> =
  | `on${Capitalize<T>}Created`
  | `on${Capitalize<T>}Updated`
  | `on${Capitalize<T>}Deleted`;

type EventosUsuario = EventosCRUD<"usuario">;
// "onUsuarioCreated" | "onUsuarioUpdated" | "onUsuarioDeleted"

type RutasApi<T extends string> = `/api/${T}` | `/api/${T}/:id`;
type RutasUsuario = RutasApi<"usuarios">;
// "/api/usuarios" | "/api/usuarios/:id"

9. Módulos y namespaces

Módulos ES (recomendado)

// matematicas.ts
export function sumar(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159265358979;

export interface Punto {
  x: number;
  y: number;
}

export default class Calculadora {
  private historial: number[] = [];

  operar(a: number, operacion: (x: number) => number): number {
    const resultado = operacion(a);
    this.historial.push(resultado);
    return resultado;
  }
}
// app.ts
import Calculadora, { sumar, PI, type Punto } from "./matematicas.js";

const calc = new Calculadora();
console.log(sumar(2, 3));
console.log(PI);

// Re-exportar
export { sumar as suma } from "./matematicas.js";
export * from "./matematicas.js";
export * as Matematicas from "./matematicas.js";

Organización de módulos en proyectos grandes

src/
├── dominio/
│   ├── usuario/
│   │   ├── usuario.types.ts      # Solo tipos
│   │   ├── usuario.schema.ts     # Validación (Zod)
│   │   ├── usuario.service.ts    # Lógica de negocio
│   │   ├── usuario.repo.ts       # Acceso a datos
│   │   └── index.ts              # Barrel export
│   └── producto/
│       └── ...
├── infraestructura/
│   ├── base-de-datos/
│   ├── http/
│   └── cache/
├── shared/
│   ├── tipos/
│   ├── utilidades/
│   └── errores/
└── index.ts

10. Decoradores

Los decoradores son una característica experimental (ahora estable en TS 5.0+) que permite añadir metadatos y modificar el comportamiento de clases y sus miembros.

// tsconfig: "experimentalDecorators": true (TS < 5.0)
// En TS 5.0+ los decoradores siguen el estándar TC39

// Decorador de clase
function Registrable(clase: new (...args: any[]) => any) {
  console.log(`Clase registrada: ${clase.name}`);
  return clase;
}

// Decorador con parámetros (factory)
function Componente(opciones: { selector: string; plantilla: string }) {
  return function(clase: new (...args: any[]) => any) {
    (clase as any).selector = opciones.selector;
    (clase as any).plantilla = opciones.plantilla;
    return clase;
  };
}

// Decorador de método
function Memoize(
  _target: any,
  _propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const metodoOriginal = descriptor.value;
  const cache = new Map<string, any>();

  descriptor.value = function(...args: any[]) {
    const clave = JSON.stringify(args);
    if (cache.has(clave)) {
      console.log("Resultado desde caché");
      return cache.get(clave);
    }
    const resultado = metodoOriginal.apply(this, args);
    cache.set(clave, resultado);
    return resultado;
  };

  return descriptor;
}

// Decorador de propiedad
function Validar(min: number, max: number) {
  return function(target: any, propertyKey: string) {
    let valor: number;

    Object.defineProperty(target, propertyKey, {
      get: () => valor,
      set: (nuevoValor: number) => {
        if (nuevoValor < min || nuevoValor > max) {
          throw new RangeError(
            `${propertyKey} debe estar entre ${min} y ${max}`
          );
        }
        valor = nuevoValor;
      },
    });
  };
}

@Registrable
@Componente({ selector: "app-usuario", plantilla: "<div>...</div>" })
class ServicioUsuario {
  @Validar(0, 120)
  edad!: number;

  @Memoize
  calcularDescuento(precio: number): number {
    console.log("Calculando descuento...");
    return precio * 0.9;
  }
}

11. TypeScript con React

Configuración

# Crear proyecto con Vite (recomendado en 2026)
npm create vite@latest mi-app -- --template react-ts
cd mi-app
npm install

Componentes tipados

// Interfaces para props
interface BotonProps {
  texto: string;
  variante?: "primario" | "secundario" | "peligro";
  deshabilitado?: boolean;
  onClick?: (evento: React.MouseEvent<HTMLButtonElement>) => void;
  children?: React.ReactNode;
  className?: string;
}

// Componente funcional
const Boton: React.FC<BotonProps> = ({
  texto,
  variante = "primario",
  deshabilitado = false,
  onClick,
  children,
  className = "",
}) => {
  const estilos = {
    primario: "bg-blue-600 text-white",
    secundario: "bg-gray-200 text-gray-800",
    peligro: "bg-red-600 text-white",
  };

  return (
    <button
      className={`px-4 py-2 rounded ${estilos[variante]} ${className}`}
      disabled={deshabilitado}
      onClick={onClick}
    >
      {children ?? texto}
    </button>
  );
};

export default Boton;

Hooks tipados

import { useState, useEffect, useCallback, useRef } from "react";

interface Usuario {
  id: number;
  nombre: string;
  email: string;
}

// useState con tipo explícito
function useUsuarios() {
  const [usuarios, setUsuarios] = useState<Usuario[]>([]);
  const [cargando, setCargando] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);

  const cargarUsuarios = useCallback(async () => {
    setCargando(true);
    setError(null);
    try {
      const res = await fetch("/api/usuarios");
      const datos: Usuario[] = await res.json();
      setUsuarios(datos);
    } catch (e) {
      setError(e instanceof Error ? e.message : "Error desconocido");
    } finally {
      setCargando(false);
    }
  }, []);

  useEffect(() => {
    cargarUsuarios();
  }, [cargarUsuarios]);

  return { usuarios, cargando, error, recargar: cargarUsuarios };
}

// useRef tipado
function EntradaAutoFoco() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} type="text" placeholder="Este input se enfoca solo" />;
}

// useReducer tipado
type AccionContador =
  | { tipo: "incrementar" }
  | { tipo: "decrementar" }
  | { tipo: "reiniciar" }
  | { tipo: "establecer"; valor: number };

interface EstadoContador {
  conteo: number;
  historial: number[];
}

function reductorContador(
  estado: EstadoContador,
  accion: AccionContador
): EstadoContador {
  switch (accion.tipo) {
    case "incrementar":
      return {
        conteo: estado.conteo + 1,
        historial: [...estado.historial, estado.conteo + 1],
      };
    case "decrementar":
      return {
        conteo: estado.conteo - 1,
        historial: [...estado.historial, estado.conteo - 1],
      };
    case "reiniciar":
      return { conteo: 0, historial: [] };
    case "establecer":
      return {
        conteo: accion.valor,
        historial: [...estado.historial, accion.valor],
      };
  }
}

Context API tipado

import { createContext, useContext, useState, type ReactNode } from "react";

interface TemaContexto {
  tema: "claro" | "oscuro";
  cambiarTema: () => void;
}

const TemaContexto = createContext<TemaContexto | undefined>(undefined);

export function ProveedorTema({ children }: { children: ReactNode }) {
  const [tema, setTema] = useState<"claro" | "oscuro">("claro");

  const cambiarTema = () => {
    setTema(prev => (prev === "claro" ? "oscuro" : "claro"));
  };

  return (
    <TemaContexto.Provider value={{ tema, cambiarTema }}>
      {children}
    </TemaContexto.Provider>
  );
}

// Hook personalizado con verificación de contexto
export function useTema(): TemaContexto {
  const contexto = useContext(TemaContexto);
  if (!contexto) {
    throw new Error("useTema debe usarse dentro de ProveedorTema");
  }
  return contexto;
}

12. TypeScript con Node.js y Express

Configuración del proyecto

npm init -y
npm install express
npm install -D typescript @types/node @types/express tsx
npx tsc --init

Servidor Express tipado

import express, {
  type Request,
  type Response,
  type NextFunction,
  Router,
} from "express";

// Interfaces para Request y Response personalizados
interface UsuarioRequest extends Request {
  usuario?: {
    id: number;
    rol: "admin" | "usuario";
  };
}

// DTOs (Data Transfer Objects)
interface CrearUsuarioDto {
  nombre: string;
  email: string;
  contraseña: string;
}

interface RespuestaApi<T> {
  exito: boolean;
  datos?: T;
  mensaje?: string;
  errores?: string[];
}

// Middleware tipado
const autenticacion = (
  req: UsuarioRequest,
  res: Response,
  next: NextFunction
): void => {
  const token = req.headers.authorization?.split(" ")[1];

  if (!token) {
    res.status(401).json({ exito: false, mensaje: "Token requerido" });
    return;
  }

  // Verificar token...
  req.usuario = { id: 1, rol: "admin" };
  next();
};

// Controlador tipado
const usuariosRouter = Router();

usuariosRouter.post(
  "/",
  async (
    req: Request<{}, RespuestaApi<{ id: number }>, CrearUsuarioDto>,
    res: Response<RespuestaApi<{ id: number }>>
  ) => {
    try {
      const { nombre, email, contraseña } = req.body;

      // Validar y crear usuario...
      const nuevoId = Math.floor(Math.random() * 1000);

      res.status(201).json({
        exito: true,
        datos: { id: nuevoId },
        mensaje: "Usuario creado exitosamente",
      });
    } catch (error) {
      res.status(500).json({
        exito: false,
        mensaje: "Error interno del servidor",
      });
    }
  }
);

// App principal
const app = express();
app.use(express.json());
app.use("/api/usuarios", autenticacion, usuariosRouter);

app.listen(3000, () => {
  console.log("Servidor corriendo en http://localhost:3000");
});

export default app;

13. Utilidades del sistema de tipos

TypeScript incluye tipos de utilidad predefinidos que simplifican transformaciones comunes.

interface Usuario {
  id: number;
  nombre: string;
  email: string;
  contraseña: string;
  creadoEn: Date;
  actualizadoEn: Date;
}

// Partial — hace todas las propiedades opcionales
type ActualizarUsuario = Partial<Usuario>;

// Required — hace todas las propiedades requeridas
type UsuarioCompleto = Required<Partial<Usuario>>;

// Readonly — hace todas las propiedades de solo lectura
type UsuarioInmutable = Readonly<Usuario>;

// Pick — seleccionar propiedades
type PerfilPublico = Pick<Usuario, "id" | "nombre" | "email">;

// Omit — excluir propiedades
type UsuarioSinContraseña = Omit<Usuario, "contraseña">;

// Record — crear tipos de diccionario
type CacheUsuarios = Record<number, Usuario>;
type EstadosPedido = Record<"pendiente" | "procesando" | "enviado" | "entregado", number>;

// Exclude — excluir tipos de una unión
type SinNull = Exclude<string | null | undefined, null | undefined>; // string

// Extract — mantener solo los tipos que coinciden
type SoloStrings2 = Extract<string | number | boolean, string>; // string

// NonNullable — quitar null y undefined
type SiempreString = NonNullable<string | null | undefined>; // string

// ReturnType — tipo de retorno de una función
function obtenerUsuario() {
  return { id: 1, nombre: "Ana" };
}
type TipoUsuario = ReturnType<typeof obtenerUsuario>; // { id: number; nombre: string }

// Parameters — tipos de los parámetros de una función
type ParamsObtenerUsuario = Parameters<typeof obtenerUsuario>; // []

// InstanceType — tipo de instancia de una clase
class ConexionBD {
  constructor(public url: string) {}
}
type TipoConexion = InstanceType<typeof ConexionBD>; // ConexionBD

// Awaited — tipo desenvuelto de una Promesa
type DatosApi = Awaited<Promise<{ usuarios: Usuario[] }>>;
// { usuarios: Usuario[] }

14. Configuración avanzada del compilador

tsconfig.json profesional

{
  "compilerOptions": {
    // Objetivo y módulos
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "NodeNext",
    "moduleResolution": "NodeNext",

    // Rutas
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@dominio/*": ["src/dominio/*"],
      "@shared/*": ["src/shared/*"]
    },
    "rootDir": "src",
    "outDir": "dist",

    // Estrictez (activar TODO en proyectos nuevos)
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true,

    // Calidad de código
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,

    // Emisión
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": false,
    "importHelpers": true,

    // Interoperabilidad
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

Aliases de rutas con Vite

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
});

15. Patrones profesionales y buenas prácticas

Patrón Repository con tipos

// Definir contratos explícitos
interface IRepositorio<T extends { id: number }> {
  crear(datos: Omit<T, "id">): Promise<T>;
  obtenerPorId(id: number): Promise<T | null>;
  obtenerTodos(filtros?: Partial<T>): Promise<T[]>;
  actualizar(id: number, datos: Partial<T>): Promise<T | null>;
  eliminar(id: number): Promise<boolean>;
}

// Implementación concreta
class RepositorioUsuarioPostgres implements IRepositorio<Usuario> {
  async crear(datos: Omit<Usuario, "id">): Promise<Usuario> {
    // INSERT INTO usuarios...
    throw new Error("No implementado");
  }

  async obtenerPorId(id: number): Promise<Usuario | null> {
    // SELECT * FROM usuarios WHERE id = $1
    throw new Error("No implementado");
  }

  // ...resto de métodos
  async obtenerTodos(): Promise<Usuario[]> { return []; }
  async actualizar(): Promise<null> { return null; }
  async eliminar(): Promise<boolean> { return false; }
}

Validación con Zod (2026 — estándar de facto)

import { z } from "zod";

// Definir schema de validación
const schemaUsuario = z.object({
  nombre: z.string().min(2, "Mínimo 2 caracteres").max(100),
  email: z.string().email("Email inválido"),
  edad: z.number().int().min(0).max(120).optional(),
  rol: z.enum(["admin", "usuario", "moderador"]).default("usuario"),
  contraseña: z
    .string()
    .min(8, "Mínimo 8 caracteres")
    .regex(/[A-Z]/, "Debe contener una mayúscula")
    .regex(/[0-9]/, "Debe contener un número"),
});

// Inferir tipo desde el schema — una sola fuente de verdad
type CrearUsuarioDto = z.infer<typeof schemaUsuario>;

// Validar en runtime
function crearUsuario(datos: unknown): CrearUsuarioDto {
  const resultado = schemaUsuario.safeParse(datos);

  if (!resultado.success) {
    const errores = resultado.error.flatten().fieldErrors;
    throw new Error(`Validación fallida: ${JSON.stringify(errores)}`);
  }

  return resultado.data;
}

Error handling tipado

// Errores de dominio tipados
class ErrorDominio extends Error {
  constructor(
    mensaje: string,
    public readonly codigo: string,
    public readonly detalles?: unknown
  ) {
    super(mensaje);
    this.name = this.constructor.name;
  }
}

class ErrorNoEncontrado extends ErrorDominio {
  constructor(recurso: string, id: number | string) {
    super(`${recurso} con id ${id} no encontrado`, "NOT_FOUND");
  }
}

class ErrorValidacion extends ErrorDominio {
  constructor(campo: string, mensaje: string) {
    super(`Error de validación en ${campo}: ${mensaje}`, "VALIDATION_ERROR");
  }
}

// Result pattern — errores sin excepciones
type Exito<T> = { tipo: "exito"; valor: T };
type Fallo<E> = { tipo: "fallo"; error: E };
type Resultado<T, E = ErrorDominio> = Exito<T> | Fallo<E>;

function exito<T>(valor: T): Exito<T> {
  return { tipo: "exito", valor };
}

function fallo<E>(error: E): Fallo<E> {
  return { tipo: "fallo", error };
}

// Uso
async function obtenerUsuario(id: number): Promise<Resultado<Usuario>> {
  const usuario = await repositorio.obtenerPorId(id);

  if (!usuario) {
    return fallo(new ErrorNoEncontrado("Usuario", id));
  }

  return exito(usuario);
}

// Consumo con discriminación
const resultado = await obtenerUsuario(1);
if (resultado.tipo === "exito") {
  console.log(resultado.valor.nombre);
} else {
  console.error(resultado.error.mensaje);
}

16. Testing con TypeScript

Vitest (recomendado en 2026)

npm install -D vitest @vitest/coverage-v8
// usuario.service.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { ServicioUsuario } from "./usuario.service";
import type { IRepositorioUsuario } from "./usuario.repo";

// Mock tipado del repositorio
const repoMock: IRepositorioUsuario = {
  crear: vi.fn(),
  obtenerPorId: vi.fn(),
  obtenerTodos: vi.fn(),
  actualizar: vi.fn(),
  eliminar: vi.fn(),
};

describe("ServicioUsuario", () => {
  let servicio: ServicioUsuario;

  beforeEach(() => {
    servicio = new ServicioUsuario(repoMock);
    vi.clearAllMocks();
  });

  describe("obtenerUsuario", () => {
    it("retorna el usuario cuando existe", async () => {
      const usuarioEsperado: Usuario = {
        id: 1,
        nombre: "Ana García",
        email: "ana@ejemplo.com",
        contraseña: "hasheada",
        creadoEn: new Date(),
        actualizadoEn: new Date(),
      };

      vi.mocked(repoMock.obtenerPorId).mockResolvedValueOnce(usuarioEsperado);

      const resultado = await servicio.obtenerUsuario(1);

      expect(resultado.tipo).toBe("exito");
      if (resultado.tipo === "exito") {
        expect(resultado.valor).toEqual(usuarioEsperado);
      }
    });

    it("retorna error cuando el usuario no existe", async () => {
      vi.mocked(repoMock.obtenerPorId).mockResolvedValueOnce(null);

      const resultado = await servicio.obtenerUsuario(999);

      expect(resultado.tipo).toBe("fallo");
      if (resultado.tipo === "fallo") {
        expect(resultado.error.codigo).toBe("NOT_FOUND");
      }
    });
  });
});

17. Monorepos y proyectos grandes

Estructura con pnpm workspaces

monorepo/
├── package.json               # Root workspace
├── pnpm-workspace.yaml
├── packages/
│   ├── tipos-compartidos/     # @mi-empresa/tipos
│   │   ├── package.json
│   │   ├── tsconfig.json
│   │   └── src/index.ts
│   ├── utilidades/            # @mi-empresa/utils
│   │   └── ...
│   └── validaciones/          # @mi-empresa/validaciones
│       └── ...
└── apps/
    ├── web/                   # Next.js
    ├── api/                   # Express/Fastify
    └── admin/                 # Vite + React
# pnpm-workspace.yaml
packages:
  - "packages/*"
  - "apps/*"
// packages/tipos-compartidos/package.json
{
  "name": "@mi-empresa/tipos",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  }
}

Referencias de proyectos en TypeScript

// tsconfig.json en la raíz
{
  "files": [],
  "references": [
    { "path": "packages/tipos-compartidos" },
    { "path": "packages/utilidades" },
    { "path": "apps/api" },
    { "path": "apps/web" }
  ]
}

18. Novedades de TypeScript 5.x (2025-2026)

TypeScript 5.5 — Control de flujo mejorado

// Predicados de tipo inferidos automáticamente (TS 5.5+)
const usuarios = [
  { id: 1, nombre: "Ana" },
  null,
  { id: 2, nombre: "Luis" },
  undefined,
];

// Antes necesitabas escribir el type guard manualmente
const usuariosValidos = usuarios.filter(u => u !== null && u !== undefined);
// En TS 5.5, TypeScript infiere el tipo correctamente: { id: number; nombre: string }[]

TypeScript 5.6 — Tipos de iteradores

// Iteradores con tipo en TS 5.6+
function* generarIds(): Generator<number, void, unknown> {
  let id = 1;
  while (true) {
    yield id++;
  }
}

async function* cargarPaginas<T>(
  url: string
): AsyncGenerator<T[], void, unknown> {
  let pagina = 1;
  while (true) {
    const res = await fetch(`${url}?pagina=${pagina}`);
    const datos: T[] = await res.json();
    if (datos.length === 0) return;
    yield datos;
    pagina++;
  }
}

Const Type Parameters

// Antes: el tipo se infería de forma amplia
function crearTupla<T>(a: T, b: T): [T, T] {
  return [a, b];
}
const par = crearTupla("hola", "mundo"); // tipo: [string, string]

// Con const — infiere tipos literales
function crearTuplaConst<const T>(a: T, b: T): [T, T] {
  return [a, b];
}
const parConst = crearTuplaConst("hola", "mundo"); // tipo: ["hola", "mundo"]

Satisfies operator (desde TS 4.9, muy usado en 2026)

type ColoresPaleta = "rojo" | "verde" | "azul";
type Paleta = Record<ColoresPaleta, string | [number, number, number]>;

// satisfies — valida el tipo pero mantiene el tipo literal inferido
const paleta = {
  rojo: [255, 0, 0],
  verde: "#00ff00",
  azul: [0, 0, 255],
} satisfies Paleta;

// Ahora paleta.rojo es [number, number, number] (no string | [...])
paleta.rojo.map(c => c * 2); // ✅ TypeScript sabe que es un array
paleta.verde.toUpperCase();  // ✅ TypeScript sabe que es string

Recursos recomendados (2026)

Documentación oficial

Librerías esenciales del ecosistema

  • Zod — validación de esquemas con inferencia de tipos
  • tRPC — APIs tipadas de extremo a extremo
  • Prisma — ORM con tipos generados automáticamente
  • TanStack Query — gestión de estado asíncrono
  • Vitest — testing rápido y tipado

Herramientas de desarrollo

  • tsx — ejecutar TypeScript directamente (desarrollo)
  • tsup — bundler para librerías TS
  • oxlint / ESLint con typescript-eslint — linting
  • Prettier — formateo de código

Conclusión

TypeScript ha pasado de ser una opción a convertirse en el estándar de la industria en 2026. Su sistema de tipos progresivo te permite adoptarlo gradualmente en proyectos existentes y escalarlo a medida que tu equipo y proyecto crecen.

La clave para dominar TypeScript es:

  1. Activar strict: true desde el inicio
  2. Evitar any — usa unknown y type guards
  3. Deja que TypeScript infiera cuando pueda; solo anota cuando sea necesario
  4. Una sola fuente de verdad — usa Zod o similar para que tipos y validaciones estén sincronizados
  5. Leer los errores del compilador — son tus mejores aliados, no tus enemigos

¡Ahora estás listo para escribir TypeScript de nivel profesional!


Última actualización: 2026 · TypeScript 5.6+