Usar TypeScript con Node.js en 2026 es más sencillo que hace tres años, pero requiere decisiones concretas en el tsconfig sobre el sistema de módulos, el target de compilación y cómo ejecutar el código durante el desarrollo. Esta guía cubre la configuración recomendada para proyectos Node.js modernos con ESM nativo, cómo tipar Express con genéricos en los handlers y cómo resolver los paths alias tanto en compilación como en runtime.
tsconfig recomendado para Node.js 2026
// tsconfig.json para Node.js 20+
{
"compilerOptions": {
"target": "ES2022", // Node 20 soporta ES2022 completo
"module": "NodeNext", // ESM nativo con resolución Node
"moduleResolution": "NodeNext", // resolución coherente con module
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
// package.json (necesario para ESM nativo):
{
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": { "node": ">=20" }
}
ESM nativo: extensiones .js en los imports
Con module: NodeNext, los imports de archivos locales deben incluir la extensión .js sí, .js aunque el archivo fuente sea .ts porque TypeScript respeta las reglas de resolución de Node.js ESM:
// src/servidor.ts
import { crearApp } from "./app.js"; // .js, no .ts
import { configurar } from "./config/index.js"; // index.js explícito
// Si el módulo no usa "type": "module" pero quieres ESM en archivos sueltos:
// Usa extensión .mts para el archivo fuente ? emite .mjs
// Importar JSON:
import datos from "./datos.json" with { type: "json" };
// Requiere "resolveJsonModule": true en tsconfig
// Importar tipos sin costo en runtime:
import type { Usuario } from "./tipos.js";
tsx: ejecutar TypeScript sin compilar
Durante el desarrollo, compilar con tsc en cada cambio es lento. tsx (no confundir con el JSX de React) ejecuta TypeScript directamente usando esbuild bajo el capó:
// Instalar:
// npm install -D tsx
// Ejecutar directamente:
// tsx src/index.ts
// Con watch mode:
// tsx watch src/index.ts
// package.json:
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
// tsx respeta el tsconfig.json, resuelve paths alias y maneja ESM correctamente
// No hace type-checking (para eso usa tsc --noEmit en CI)
Express tipado con genéricos
Express tiene tipos genéricos en Request y Response que permiten tipar los parámetros de ruta, el query string y el body sin hacer casting manual:
import express, { Request, Response, NextFunction } from "express";
// Tipos de los parámetros de la ruta:
interface ParamsRuta { id: string }
// Tipo del body de la petición:
interface BodyCrearUsuario {
nombre: string;
email: string;
rol?: "admin" | "usuario";
}
// Tipo del query string:
interface QueryListar {
pagina?: string;
limite?: string;
filtro?: string;
}
// Handler con todos los genéricos tipados:
async function obtenerUsuario(
req: Request<ParamsRuta, Usuario | null, never, never>,
res: Response<Usuario | null>
) {
const id = parseInt(req.params.id, 10);
const usuario = await servicioUsuarios.buscar(id);
if (!usuario) {
res.status(404).json(null);
return;
}
res.json(usuario);
}
const app = express();
app.use(express.json());
app.get("/usuarios/:id", obtenerUsuario);
Ampliar el tipo de Request con module augmentation
// src/types/express.d.ts
import "express";
declare module "express-serve-static-core" {
interface Request {
usuario?: {
id: number;
email: string;
rol: "admin" | "usuario";
};
}
}
// Middleware de autenticación:
function autenticar(req: Request, res: Response, next: NextFunction) {
// ... verificar JWT ...
req.usuario = { id: 1, email: "[email protected]", rol: "admin" };
next();
}
// En cualquier handler posterior, req.usuario está disponible y tipado
Resolver paths alias en runtime con tsconfig-paths
// tsconfig.json con alias:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@servicios/*": ["src/servicios/*"]
}
}
}
// Para ejecutar el JS compilado (tsc emite los imports como alias, no rutas reales):
// npm install -D tsconfig-paths
// En el script de arranque:
// node -r tsconfig-paths/register dist/index.js
// O con tsx (resuelve los alias automáticamente en desarrollo):
// tsx src/index.ts
// Para esbuild (producción), usar el plugin de paths:
// import { TsconfigPathsPlugin } from "@esbuild-plugins/tsconfig-paths";
La configuración de TypeScript para Node.js es más sencilla de lo que parece si se adopta ESM desde el principio. El punto de fricción mayor suele ser la extensión .js en los imports de archivos locales: aunque parezca raro, es la forma correcta para que la resolución ESM de Node.js funcione tanto con tsc como con tsx.
Imagen: Pexels / César Gaviria
