Express.js
TypeScript con Express
Explica cómo tipar handlers, requests, locals, middleware y dependencias en Express.js sin sustituir la validación runtime ni abusar de module augmentation.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Express.js
Explica cómo tipar handlers, requests, locals, middleware y dependencias en Express.js sin sustituir la validación runtime ni abusar de module augmentation.
TypeScript mejora contratos dentro del código, pero no valida lo que llega por HTTP. En Express, los boundaries deben combinar tipos estáticos con validación de runtime.
bytes externos
↓ parser
unknown
↓ runtime validation
valor confiable y tipado
↓
caso de usoUn cast no convierte datos:
const input = request.body as CreateOrderInput;Solo le pide al compilador que confíe.
import type { Request, Response, NextFunction, RequestHandler } from 'express';Request admite genéricos para params, response body, request body y query, pero tiparlos no valida runtime.
type Params = { orderId: string };
type ResponseBody = { data: OrderResponse };
type RequestBody = never;
type Query = { include?: string };
const handler: RequestHandler<Params, ResponseBody, RequestBody, Query> = async (
request,
response,
) => { ... };const result = CreateOrderSchema.safeParse(request.body);
if (!result.success) throw toValidationError(result.error);
const input = result.data;Después de validar, input tiene forma conocida.
Prefiere unknown en boundaries antes que any. any desactiva comprobación y se propaga.
Puedes guardar datos en res.locals con un tipo:
type Locals = {
actor: Actor;
input: CreateOrderInput;
};
const handler: RequestHandler<{}, CreateOrderResponse, unknown, {}, Locals> = async (
_request,
response,
) => {
const order = await createOrder.execute({
actor: response.locals.actor,
input: response.locals.input,
});
response.status(201).json({ data: order });
};El tipo ayuda a composición, pero el middleware debe realmente asignar esos valores.
Module augmentation puede añadir propiedades:
declare global {
namespace Express {
interface Request {
requestId: string;
}
}
}Úsalo para propiedades realmente globales y siempre presentes después de un middleware obligatorio. Para contexto de rutas específicas, res.locals o parámetros explícitos pueden ser más honestos.
Una augmentation opcional obliga a checks en toda la app y puede esconder dependencias de orden.
JavaScript puede lanzar cualquier valor. En catch, trata error como unknown:
catch (error: unknown) {
if (error instanceof OrderNotFoundError) { ... }
throw error;
}Diseña errores o unions con códigos estables. No confíes en error.message para lógica.
Algunos módulos prefieren resultados explícitos:
type CreateOrderResult =
| { ok: true; order: Order }
| { ok: false; reason: 'insufficient_stock'; productId: string };Esto hace exhaustivas decisiones esperadas. Excepciones pueden reservarse para fallos inesperados. No existe una única política universal; sé consistente.
CreateOrderHttpBody
→ CreateOrderCommand
→ Order domain model
→ OrderResponseNo reutilices automáticamente el mismo type para todos. El body puede aceptar strings/coercion; dominio necesita valores normalizados; response omite campos internos.
Para evitar mezclar IDs:
type OrderId = string & { readonly __brand: 'OrderId' };
type TenantId = string & { readonly __brand: 'TenantId' };El branding ayuda dentro del código, pero debe crearse después de validación. No prueba que el recurso exista.
null puede significar “borrar”.undefined no se serializa en JSON.Define semántica especialmente en PATCH. exactOptionalPropertyTypes ayuda a distinguir optional de undefined explícito.
Los tipos de Express pueden ser amplios porque el parser admite arrays/objetos. Valida y normaliza antes de usar:
const query = QuerySchema.parse(request.query);No asumas string sin contrato.
En Express 5 los handlers pueden retornar Promise y rechazos se propagan. Tipos de @types/express deben ser compatibles con la versión instalada.
Mantén express y @types/express alineados; revisa notas de versión durante upgrades.
Con module: NodeNext, imports compilados suelen necesitar extensión .js:
import { createApp } from './app.js';Aunque el archivo fuente sea .ts, el runtime ejecutará .js. La configuración debe coincidir con package type y build.
Versiones recientes de Node pueden eliminar tipos de sintaxis soportada, pero:
tsc --noEmit sigue siendo necesario para verificar.
Tipar cada handler con cinco genéricos largos puede reducir legibilidad. Aliases y factories ayudan:
type AuthenticatedHandler<P, Res, Body, Query> = RequestHandler<
P,
Res,
Body,
Query,
AuthenticatedLocals
>;No conviertas tipos en una segunda arquitectura difícil de leer.
Herramientas como Zod pueden inferir tipos:
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;Esto reduce drift, pero el schema de transporte no siempre debe ser el tipo de dominio. Transforma después de validar.
Generar OpenAPI desde schemas evita duplicación parcial. Aun necesitas documentar status, auth, headers, examples y semántica.
tsc --noEmit en CI.expectTypeOf o tsd para APIs públicas cuando aporta valor.No pruebes solo compilación: un cast puede hacer que compile y falle.
const CreateOrderBodySchema = z.object({
customerId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
})).min(1),
}).strict();
const validateCreateOrder: RequestHandler<{}, unknown, unknown, {}, Locals> = (
request,
response,
next,
) => {
const parsed = CreateOrderBodySchema.safeParse(request.body);
if (!parsed.success) return next(toValidationError(parsed.error));
response.locals.input = parsed.data;
next();
};Flujo: parser produce unknown, schema verifica, locals guarda input, handler recibe contrato.
req.body as Type.any en boundaries.as?Seguridad de dependencias y supply chain protege el código que la aplicación instala y ejecuta.