The middleware validates a token and puts a user on req.user. The next route receives the same object at runtime, but TypeScript still complains that the property is missing or may be undefined.
That isn't a compiler failure. Express runs functions in an order configured at runtime. TypeScript checks each handler without proving which middleware reached it. The safe solution combines an optional global type with a check that makes the route's stronger guarantee explicit.
This article uses req.user as its example, but the same boundary works for req.locale, req.requestId, and other context data. For the logic between a request, a service, and a response, see organizing TypeScript service boundaries.

Short answer
- Declare properties added to the request as optional.
- Validate the value before using it in a protected route.
- Use a local type, an assertion function, or
res.localsto express the guarantee.- Test the route without the middleware too. The type doesn't replace runtime protection.
Why doesn't TypeScript follow middleware order?
Express lets middleware change the request, change the response, end the cycle, or call next(). The official "Writing middleware for use in Express apps" guide also says that loading order matters: functions registered first run first (Express, "Writing middleware for use in Express apps").
That order belongs to application composition. It can change when a route gets a new prefix, when a router is mounted elsewhere, or when someone reuses the handler without authentication middleware. A signature that marks user as always present would make a promise for all of those calls.
So the useful question isn't "how do I make TypeScript trust my middleware?" It is "where can the application prove that the value exists?" That question avoids two unsafe shortcuts: using as any and making user required on every request.
How do you declare a property added to the request?
Use declaration merging in a .d.ts file included by the project. The Express guide "Overriding the Express API" uses this pattern to describe user? in the global Express namespace (Express, "Overriding the Express API").
The example below is illustrative. A real project needs to choose its user type and confirm that the file is inside the tsconfig.json include:
// src/types/express.d.ts
export {};
type User = {
id: string;
role: "admin" | "member";
};
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
The property is optional on purpose. A public route may use a different set of functions. Express itself warns that TypeScript doesn't know which middleware ran before a handler, so a required field would be a false guarantee (Express, "Overriding the Express API").
This file describes a possible contract. It doesn't create req.user at runtime, validate a token, or stop a route from forgetting its authentication middleware. Declaration merging only combines interface declarations with the same name in the type the compiler reads, as the TypeScript handbook explains (TypeScript, "Declaration Merging").
How do you validate the data before the handler?
Check it at the route boundary. Authentication middleware should end the request when it can't find a user and call next() only after validation:
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();
}
The check protects application behavior, but it still doesn't change the signature of every handler Express receives. Inside a protected handler, an assertion function can turn the check into a local guarantee:
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");
}
}
The return type asserts req is ... is a type assertion. Once the function returns without throwing, TypeScript narrows req in that flow. The TypeScript handbook, "Narrowing", documents this use of predicates and control-flow analysis (TypeScript, "Narrowing").
The boundary is visible in the handler:
app.get("/me", requireUser, (req, res) => {
assertUser(req);
res.json({
id: req.user.id,
role: req.user.role,
});
});
The example still needs the error handling that fits your project. The assertion isn't a substitute for authentication. It repeats the proof at the consumer boundary so the code remains correct if someone reuses the handler in another composition.
When is res.locals a better choice?
Use res.locals when the data belongs to that response cycle and shouldn't look like a universal request property. Express keeps res.locals available for one request-response cycle without sharing the value between requests (Express, "Response").
This option keeps context produced by middleware separate from input received from the client:
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 });
}
The generic signature must match the Express types installed in the project. If the local definition doesn't accept those parameters, keep the same idea with a named handler type and verify the result with tsc --noEmit. The point isn't to memorize the signature. It is to stop the consumer from using the value before crossing the boundary.
How do you choose between request, res.locals, and a local type?
Use a request property when several middleware functions need to read the same context during a request. Use res.locals when the value is created for the response or for one specific handler chain. Use a local type when only one part of the application can receive the guarantee.
| Situation | Choice | Reason |
|---|---|---|
| The value may exist on any request, but not every request | optional Request property |
represents possibility without promising presence |
| The value was loaded for one handler chain | typed res.locals |
limits context to the response cycle |
| One route requires the value | assertion or local handler type | makes the precondition explicit at the use site |
| An external library creates the value | type augmentation | describes the contract the code already expects |
Don't use declaration merging to erase the difference between a public and protected route. Express allows Request extensions, but recommends optional fields because the compiler doesn't know the earlier composition. The type should match the actual reach of the guarantee.
The same separation between accepted data and permitted effects appears when you validate data before executing an action: establish the precondition first, then let the handler work with the narrower contract.
How do you verify that the typing protects the route?
Start with the compiler, not the editor. Run npx tsc --noEmit and confirm two things: an unknown key fails, and the protected handler can access req.user.id after the assertion. The code in this article is illustrative, so adjust imports and generics before running it.
Then test behavior. A request without credentials should return an unauthorized response. A request with credentials should call next() and reach the handler with the expected user. Also test a composition where the handler is mounted without requireUser; it should fail in a controlled way, not only produce a TypeScript error.
For the HTTP path, use a test that goes through the application and checks the real response. The separation between mocks and integrated behavior matters. See how to test the real path without false positives. If the application is split across projects, declaring boundaries between TypeScript projects can also help keep the type file inside the build.
What usually goes wrong?
The first mistake is declaring user: User globally. That quiets the compiler on public routes but creates a type that lies about runtime behavior. Keep user? and prove presence only where the route requires it.
The second is using req.user! in every handler. A non-null assertion can be acceptable after a short proof, but spreading it through the application hides the precondition. A central assertion function gives the check and its error message one place to evolve.
The third is creating the augmentation file and forgetting tsconfig.json. If the editor sees the file but CI doesn't, compare include, exclude, and the files loaded by the compiler. Express calls out this detail too: no configuration change is needed unless a custom include leaves the file out (Express, "Writing middleware for use in Express apps").
The fourth is trusting the type without testing composition. TypeScript describes the program you declared. It doesn't observe the order of app.use in production or know whether a route was exported without its middleware. Protection needs to exist in both layers.
Frequently asked questions
Does declaration merging make req.user required?
No. The safe pattern is user?, because the interface applies to every request. Express recommends an optional property and a check before use. A protected route can narrow the type with an assertion or local handler type, but it shouldn't change the promise made to public routes.
Should I put the user on req or in res.locals?
It depends on scope. req works when several parts of the chain read the context. res.locals helps when the value belongs to the response cycle and should be shared by handlers in that chain. In both cases, middleware still needs to validate the value at runtime.
Why doesn't a cast fix middleware order?
A cast changes what the compiler accepts at that point. It doesn't validate a token, add a property, or change execution order. Use a cast only after a concrete proof, and prefer an assertion function that keeps the check and error message together.
Conclusion
Typing data added by Express middleware requires separating three things: what any request may have, what middleware actually validates, and what one route is allowed to use.
Declare the field as optional, check it at runtime, and model the stronger guarantee near the consumer. That keeps Express flexible without asking TypeScript to accept a promise the application can't fulfill.
Sources consulted
- Express, "Writing middleware for use in Express apps", retrieved 2026-08-31.
- Express, "Overriding the Express API", retrieved 2026-08-31.
- Express, "Response", retrieved 2026-08-31.
- TypeScript, "Declaration Merging", retrieved 2026-08-31.
- TypeScript, "Narrowing", retrieved 2026-08-31.