El middleware valida un token y coloca un usuario en req.user. La ruta siguiente recibe el mismo objeto en runtime, pero TypeScript sigue avisando de que la propiedad no existe o puede ser undefined.
Eso no es un fallo del compilador. Express ejecuta funciones en un orden configurado en runtime. TypeScript comprueba cada handler sin demostrar qué middleware llegó hasta él. La solución segura combina un tipo global opcional con una comprobación que hace explícita la garantía más fuerte de la ruta.
Este artículo usa req.user como ejemplo, pero el mismo límite sirve para req.locale, req.requestId y otros datos de contexto. Para organizar la lógica entre una request, un servicio y una respuesta, consulta cómo organizar fronteras entre servicios TypeScript.

Respuesta corta
- Declara como opcionales las propiedades que el middleware añade a la request.
- Valida el dato antes de usarlo en una ruta protegida.
- Usa un tipo local, una función de aserción o
res.localspara expresar la garantía.- Prueba también la ruta sin el middleware. El tipo no sustituye la protección en runtime.
¿Por qué TypeScript no sigue el orden del middleware?
Express permite que un middleware cambie la request, cambie la response, termine el ciclo o llame a next(). La guía oficial "Writing middleware for use in Express apps" también explica que el orden de carga importa: las funciones registradas primero se ejecutan primero (Express, "Writing middleware for use in Express apps").
Ese orden pertenece a la composición de la aplicación. Puede cambiar cuando una ruta recibe un prefijo nuevo, cuando un router se monta en otro lugar o cuando alguien reutiliza el handler sin el middleware de autenticación. Una firma que marque user como siempre presente haría una promesa para todas esas llamadas.
Por eso, la pregunta útil no es "¿cómo hago que TypeScript confíe en mi middleware?". Es "¿dónde puede demostrar la aplicación que el valor existe?". Esa pregunta evita dos atajos inseguros: usar as any y hacer que user sea obligatorio en cada request.
¿Cómo se declara una propiedad añadida a la request?
Usa declaration merging en un archivo .d.ts incluido por el proyecto. La guía de Express "Overriding the Express API" usa este patrón para describir user? en el namespace global Express (Express, "Overriding the Express API").
El ejemplo siguiente es ilustrativo. Un proyecto real debe elegir su tipo de usuario y confirmar que el archivo está dentro de include en tsconfig.json:
// src/types/express.d.ts
export {};
type User = {
id: string;
role: "admin" | "member";
};
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
La propiedad es opcional a propósito. Una ruta pública puede usar un conjunto distinto de funciones. Express advierte que TypeScript no sabe qué middleware se ejecutó antes de un handler, así que un campo obligatorio sería una garantía falsa (Express, "Overriding the Express API").
Este archivo describe un contrato posible. No crea req.user en runtime, no valida un token ni impide que una ruta olvide su middleware de autenticación. Declaration merging solo combina declaraciones de interfaces con el mismo nombre en el tipo que lee el compilador, como explica el handbook de TypeScript (TypeScript, "Declaration Merging").
¿Cómo se valida el dato antes del handler?
Compruébalo en el límite de la ruta. El middleware de autenticación debe terminar la request cuando no encuentra un usuario y llamar a next() solo después de validarlo:
import type { Request, Response, NextFunction } from "express";
export function requireUser(
req: Request,
res: Response,
next: NextFunction,
): void {
if (!req.user) {
res.status(401).json({ error: "unauthorized" });
return;
}
next();
}
La comprobación protege el comportamiento de la aplicación, pero no cambia la firma de cada handler que recibe Express. Dentro de un handler protegido, una función de aserción puede convertir la comprobación en una garantía local:
import type { Request } from "express";
type AuthenticatedRequest = Request & {
user: NonNullable<Request["user"]>;
};
export function assertUser(
req: Request,
): asserts req is AuthenticatedRequest {
if (!req.user) {
throw new Error("Authenticated request expected");
}
}
El retorno asserts req is ... es una aserción de tipo. Cuando la función termina sin lanzar un error, TypeScript estrecha req en ese flujo. El handbook de TypeScript, "Narrowing", documenta este uso de predicados y análisis de flujo (TypeScript, "Narrowing").
El límite queda visible en el handler:
app.get("/me", requireUser, (req, res) => {
assertUser(req);
res.json({
id: req.user.id,
role: req.user.role,
});
});
El ejemplo todavía necesita el tratamiento de errores que corresponda a tu proyecto. La aserción no sustituye la autenticación. Repite la prueba en el límite del consumidor para que el código siga siendo correcto si alguien reutiliza el handler en otra composición.
¿Cuándo es mejor usar res.locals?
Usa res.locals cuando el dato pertenece al ciclo de esa respuesta y no debería parecer una propiedad universal de la request. Express mantiene res.locals durante un solo ciclo request-response sin compartir el valor entre requests (Express, "Response").
Esta opción separa el contexto producido por el middleware de los datos recibidos del cliente:
import type {
RequestHandler,
Response,
} from "express";
type AuthLocals = {
user: {
id: string;
role: "admin" | "member";
};
};
export const loadUser: RequestHandler<
{},
unknown,
unknown,
{},
AuthLocals
> = (req, res, next) => {
const user = req.user;
if (!user) {
res.status(401).json({ error: "unauthorized" });
return;
}
res.locals.user = user;
next();
};
export function account(
_req: unknown,
res: Response<unknown, AuthLocals>,
) {
res.json({ userId: res.locals.user.id });
}
La firma genérica debe coincidir con los tipos de Express instalados en el proyecto. Si la definición local no acepta esos parámetros, conserva la misma idea con un tipo de handler con nombre y verifica el resultado con tsc --noEmit. El objetivo no es memorizar la firma. Es impedir que el consumidor use el valor antes de cruzar el límite.
¿Cómo elegir entre request, res.locals y un tipo local?
Usa una propiedad de la request cuando varios middlewares necesiten leer el mismo contexto durante una request. Usa res.locals cuando el valor se crea para la respuesta o para una cadena concreta de handlers. Usa un tipo local cuando solo una parte de la aplicación puede recibir la garantía.
| Situación | Elección | Motivo |
|---|---|---|
| El valor puede existir en cualquier request, pero no en todas | propiedad opcional en Request |
representa la posibilidad sin prometer presencia |
| El valor se cargó para una cadena de handlers | res.locals tipado |
limita el contexto al ciclo de la respuesta |
| Una ruta necesita el valor | aserción o tipo local de handler | hace explícita la precondición en el punto de uso |
| Una biblioteca externa crea el valor | augmentation de tipos | describe el contrato que el código ya espera |
No uses declaration merging para borrar la diferencia entre una ruta pública y una protegida. Express permite extender Request, pero recomienda campos opcionales porque el compilador no conoce la composición anterior. El tipo debe coincidir con el alcance real de la garantía.
La misma separación entre datos aceptados y efectos permitidos aparece al validar los datos antes de ejecutar una acción: establece primero la precondición y deja después que el handler trabaje con un contrato más pequeño.
¿Cómo se verifica que la tipificación protege la ruta?
Empieza por el compilador, no por el editor. Ejecuta npx tsc --noEmit y confirma dos cosas: una clave desconocida falla y el handler protegido puede acceder a req.user.id después de la aserción. El código de este artículo es ilustrativo, así que ajusta los imports y genéricos antes de ejecutarlo.
Después prueba el comportamiento. Una request sin credenciales debe devolver una respuesta de no autorizado. Una request con credenciales debe llamar a next() y llegar al handler con el usuario esperado. Prueba también una composición en la que el handler se monte sin requireUser: debe fallar de forma controlada, no solo producir un error de TypeScript.
Para el camino HTTP, usa una prueba que atraviese la aplicación y compruebe la respuesta real. La separación entre mocks y comportamiento integrado importa. Consulta cómo probar el camino real sin falsos positivos. Si la aplicación está dividida en proyectos, declarar fronteras entre proyectos TypeScript también puede ayudar a mantener el archivo de tipos dentro del build.
¿Qué suele salir mal?
El primer error es declarar user: User de forma global. Eso silencia el compilador en las rutas públicas, pero crea un tipo que miente sobre el runtime. Mantén user? y demuestra la presencia solo donde la ruta necesita el valor.
El segundo es usar req.user! en cada handler. Una aserción no nula puede ser aceptable después de una prueba breve, pero repartirla por la aplicación oculta la precondición. Una función de aserción central ofrece un único lugar para ajustar la comprobación y el mensaje de error.
El tercero es crear el archivo de augmentation y olvidar tsconfig.json. Si el editor ve el archivo, pero CI no, compara include, exclude y los archivos que carga el compilador. Express también señala este detalle: no hace falta cambiar la configuración salvo que un include personalizado deje el archivo fuera (Express, "Writing middleware for use in Express apps").
El cuarto es confiar en el tipo sin probar la composición. TypeScript describe el programa que declaraste. No observa el orden de app.use en producción ni sabe si una ruta se exportó sin su middleware. La protección debe existir en las dos capas.
Preguntas frecuentes
¿Declaration merging hace obligatorio req.user?
No. El patrón seguro es user?, porque la interfaz se aplica a cada request. Express recomienda una propiedad opcional y una comprobación antes de usarla. Una ruta protegida puede estrechar el tipo con una aserción o un tipo local de handler, pero no debería cambiar la promesa hecha a las rutas públicas.
¿Pongo el usuario en req o en res.locals?
Depende del alcance. req funciona cuando varias partes de la cadena leen el contexto. res.locals ayuda cuando el valor pertenece al ciclo de la respuesta y debe compartirse entre los handlers de esa cadena. En ambos casos, el middleware todavía debe validar el valor en runtime.
¿Por qué un cast no corrige el orden del middleware?
Un cast cambia lo que el compilador acepta en ese punto. No valida un token, no añade una propiedad ni cambia el orden de ejecución. Usa un cast solo después de una prueba concreta y prefiere una función de aserción que mantenga juntas la comprobación y el mensaje de error.
Conclusión
Tipar datos añadidos por middleware en Express exige separar tres cosas: lo que cualquier request puede tener, lo que el middleware realmente valida y lo que una ruta puede usar.
Declara el campo como opcional, compruébalo en runtime y modela la garantía más fuerte cerca del consumidor. Así mantienes la flexibilidad de Express sin pedirle a TypeScript que acepte una promesa que la aplicación no puede cumplir.
Fuentes consultadas
- Express, "Writing middleware for use in Express apps", consultado el 31/08/2026.
- Express, "Overriding the Express API", consultado el 31/08/2026.
- Express, "Response", consultado el 31/08/2026.
- TypeScript, "Declaration Merging", consultado el 31/08/2026.
- TypeScript, "Narrowing", consultado el 31/08/2026.