TypeScript desde 0 a Profesional (2026)
Tutorial
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
tsxen lugar dets-nodepara 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 constcon 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ística | Interface | Type |
|---|---|---|
| Declaration merging | ✅ | ❌ |
| Extends / implements | ✅ | ❌ (usa &) |
| Tipos primitivos | ❌ | ✅ |
| Uniones / intersecciones | ❌ | ✅ |
| Tipos mapeados | ❌ | ✅ |
| Mejor para objetos/clases | ✅ | ✅ |
Regla práctica 2026: Usa
interfacepara contratos de objetos y clases. Usatypepara 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:
- Activar
strict: truedesde el inicio - Evitar
any— usaunknowny type guards - Deja que TypeScript infiera cuando pueda; solo anota cuando sea necesario
- Una sola fuente de verdad — usa Zod o similar para que tipos y validaciones estén sincronizados
- 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+