Saltar al contenido principal

TypeScript para Desarrolladores JavaScript: La Guía Práctica que Ojalá Hubiera Tenido

Aprende TypeScript si ya sabes JavaScript: tipos, interfaces, generics y los errores más comunes que cometerás. Sin teoría innecesaria, todo con ejemplos reales de proyectos.

Fran Cobos 8 min de lectura 1596 palabras

Tabla de contenidos

Llevaba 3 años escribiendo JavaScript y convencido de que TypeScript era para proyectos enterprise con 50 desarrolladores. Me equivocaba. Lo adopté en un proyecto mediano y en dos semanas detectó 4 bugs reales que habrían llegado a producción.

Esta es la guía que me habría ahorrado los primeros tropiezos.

Por qué TypeScript vale la pena (con datos reales)

Antes de entrar en código, el argumento más honesto que puedo darte:

JavaScript no te dice que esto es un error hasta que el usuario lo sufre:

function calcularPrecio(precio, descuento) {
  return precio - precio * descuento;
}

calcularPrecio("100", 0.1); // "100" - "100" * 0.1 = "100" - 10 = "10010" 🔥

TypeScript lo detecta antes de ejecutar:

function calcularPrecio(precio: number, descuento: number): number {
  return precio - precio * descuento;
}

calcularPrecio("100", 0.1); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

El error aparece en tu editor, no en producción.


Paso 1: Instalar y configurar TypeScript

npm install -D typescript ts-node @types/node
npx tsc --init

El tsconfig.json básico que uso en todos mis proyectos:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

La opción clave es "strict": true. Activa todas las comprobaciones importantes. No la desactives aunque duela al principio.


Tipos básicos: el 80% de lo que usarás

// Primitivos
const nombre: string = "Fran";
const edad: number = 30;
const activo: boolean = true;

// Arrays
const tags: string[] = ["typescript", "javascript"];
const puntuaciones: number[] = [8, 9, 7];

// Objetos con tipo inline
const usuario: { nombre: string; edad: number } = {
  nombre: "Fran",
  edad: 30
};

// Union types — cuando puede ser más de un tipo
function formatearId(id: string | number): string {
  return id.toString();
}

// Optional — cuando puede no estar
function saludar(nombre: string, apellido?: string): string {
  return apellido ? `${nombre} ${apellido}` : nombre;
}

Interfaces vs Types: la pregunta que todo el mundo hace

La respuesta corta: usa interface para objetos, type para todo lo demás.

// Interface — ideal para objetos y contratos de clase
interface Usuario {
  id: number;
  nombre: string;
  email: string;
  rol: "admin" | "editor" | "viewer";
}

// Type — ideal para unions, intersecciones y tipos complejos
type Resultado<T> = 
  | { ok: true; datos: T }
  | { ok: false; error: string };

type IdONombre = string | number;

La diferencia práctica más importante: las interfaces se pueden extender y combinar, los types no se pueden redeclarar:

interface Animal {
  nombre: string;
}

interface Animal {
  patas: number; // Válido — TypeScript las fusiona
}

// ✅ Ahora Animal tiene nombre Y patas

type Coche = { marca: string };
type Coche = { modelo: string }; // ❌ Error: Duplicate identifier 'Coche'

Para trabajar en proyectos como los que describo en construir un SaaS desde cero, tener bien tipadas las interfaces de tu dominio es la diferencia entre un código mantenible y un caos.


Generics: el superpoder de TypeScript

Los generics asustan al principio. No son más que tipos reutilizables con parámetro:

// Sin generics — necesito duplicar para cada tipo
function primeraStringArray(arr: string[]): string {
  return arr[0];
}
function primerNumberArray(arr: number[]): number {
  return arr[0];
}

// Con generics — una función para todos los tipos
function primero<T>(arr: T[]): T {
  return arr[0];
}

primero(["a", "b", "c"]); // TypeScript infiere T = string
primero([1, 2, 3]);       // TypeScript infiere T = number
primero([true, false]);   // TypeScript infiere T = boolean

El patrón de generic que más uso en APIs:

// Respuesta tipada de API
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface Producto {
  id: number;
  nombre: string;
  precio: number;
}

async function fetchProducto(id: number): Promise<ApiResponse<Producto>> {
  const res = await fetch(`/api/productos/${id}`);
  return res.json();
}

const respuesta = await fetchProducto(1);
respuesta.data.nombre; // TypeScript sabe que es string ✅
respuesta.data.precio; // TypeScript sabe que es number ✅

Tipado de funciones async y promesas

El punto donde más gente se atasca:

// Forma correcta de tipar funciones async
async function obtenerUsuario(id: number): Promise<Usuario> {
  const res = await fetch(`/api/usuarios/${id}`);
  
  if (!res.ok) {
    throw new Error(`HTTP error: ${res.status}`);
  }
  
  return res.json() as Promise<Usuario>;
}

// Con manejo de errores tipado
type ResultadoApi<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

async function obtenerUsuarioSafe(id: number): Promise<ResultadoApi<Usuario>> {
  try {
    const usuario = await obtenerUsuario(id);
    return { success: true, data: usuario };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : "Error desconocido"
    };
  }
}

Este patrón se combina muy bien con proteger tu API con JWT en Node.js, donde los tipos te obligan a manejar todos los casos de error.


Errores comunes y cómo evitarlos

❌ Error 1: Usar any para todo

// Mal — pierdes todo el beneficio de TypeScript
function procesarDatos(datos: any) {
  return datos.nombre.toUpperCase(); // Runtime error si datos no tiene nombre
}

// Bien — usa unknown y valida
function procesarDatos(datos: unknown) {
  if (typeof datos === "object" && datos !== null && "nombre" in datos) {
    const { nombre } = datos as { nombre: string };
    return nombre.toUpperCase();
  }
  throw new Error("Datos inválidos");
}

❌ Error 2: No usar type guards

// Mal — TypeScript no sabe qué tipo es
function procesarEvento(evento: MouseEvent | KeyboardEvent) {
  console.log(evento.key); // Error: 'key' no existe en MouseEvent
}

// Bien — type guard
function procesarEvento(evento: MouseEvent | KeyboardEvent) {
  if (evento instanceof KeyboardEvent) {
    console.log(evento.key); // ✅ TypeScript sabe que es KeyboardEvent
  } else {
    console.log(evento.clientX); // ✅ TypeScript sabe que es MouseEvent
  }
}

❌ Error 3: as para silenciar errores

// Trampa — le dices a TypeScript que confíe en ti ciegamente
const usuario = await fetch("/api/user").then(r => r.json()) as Usuario;
usuario.nombre; // TypeScript confía en ti, pero puede ser undefined en runtime

// Mejor — valida con zod o una función de validación
import { z } from "zod";

const UsuarioSchema = z.object({
  id: z.number(),
  nombre: z.string(),
  email: z.string().email()
});

const datos = await fetch("/api/user").then(r => r.json());
const usuario = UsuarioSchema.parse(datos); // Lanza error si no coincide

TypeScript con React y Next.js

En proyectos React, el tipado más usado:

import { FC, ReactNode, useState, useEffect } from "react";

// Componente tipado
interface TarjetaProductoProps {
  producto: Producto;
  onAgregar: (id: number) => void;
  children?: ReactNode;
}

const TarjetaProducto: FC<TarjetaProductoProps> = ({ producto, onAgregar, children }) => {
  return (
    <div className="card">
      <h2>{producto.nombre}</h2>
      <p>{producto.precio}€</p>
      <button onClick={() => onAgregar(producto.id)}>Añadir al carrito</button>
      {children}
    </div>
  );
};

// useState con tipo explícito
const [productos, setProductos] = useState<Producto[]>([]);
const [loading, setLoading] = useState<boolean>(false);

// useEffect tipado
useEffect(() => {
  const cargar = async () => {
    const data = await fetchProductos();
    setProductos(data);
  };
  cargar();
}, []);

Si usas Auth.js / NextAuth con Next.js, TypeScript te permite tipar la sesión correctamente y evitar acceder a campos que no existen.


TypeScript con Node.js y Express

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

// Extender el tipo Request para añadir propiedades custom
declare global {
  namespace Express {
    interface Request {
      usuario?: Usuario;
    }
  }
}

// Middleware tipado
const autenticar = (req: Request, res: Response, next: NextFunction): void => {
  const token = req.headers.authorization?.split(" ")[1];
  
  if (!token) {
    res.status(401).json({ error: "Token requerido" });
    return;
  }
  
  // Verificar token...
  req.usuario = { id: 1, nombre: "Fran", email: "fran@example.com", rol: "admin" };
  next();
};

// Route handler tipado
const obtenerPerfil = async (req: Request, res: Response): Promise<void> => {
  if (!req.usuario) {
    res.status(401).json({ error: "No autenticado" });
    return;
  }
  
  res.json({ usuario: req.usuario });
};

Herramientas esenciales del ecosistema TypeScript

HerramientaPara qué sirve
ZodValidación de datos en runtime con inferencia de tipos
tRPCAPIs tipadas end-to-end sin código extra
PrismaORM con tipos generados automáticamente desde el schema
ts-nodeEjecutar TypeScript directamente sin compilar
tsxAlternativa más rápida a ts-node
type-festUtilidades de tipos para casos avanzados

Para proyectos que usan IA, TypeScript brilla especialmente: puedes tipar las respuestas de la API de OpenAI o Claude y asegurarte de que parseas el JSON correctamente.


Plan de aprendizaje en 4 semanas

Semana 1: Tipos básicos, interfaces, funciones tipadas. Convierte un archivo JS tuyo a TS.

Semana 2: Generics básicos, union types, type guards, async/await. Convierte un módulo completo.

Semana 3: Tipos de utilidad (Partial, Required, Pick, Omit, Record). Empieza a usar Zod.

Semana 4: Tipos condicionales, infer, decorators si usas NestJS. Proyecto completo en TS.

El objetivo no es entender TODO TypeScript. Es eliminar el 90% de los errores de tipo que te llegarían a producción.


Recursos adicionales

  • Si usas IA para programar, leer código TypeScript tipado le da mucho más contexto a herramientas como Cursor o Copilot — te genera código mejor
  • TypeScript es casi obligatorio si sigues los patrones de NestJS sobre Express
  • Para estructurar bien tus carpetas TypeScript en proyectos React, echa un vistazo a estructura de carpetas en React/Next.js

TypeScript no es una carga — es un radar que detecta los misiles antes de que impacten. Cuanto antes lo adoptes, más bugs evitarás sin esfuerzo adicional.

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.