Lesson 71 of 80
An Express/Koa-style middleware chain type-checks each handler individually just fine — the hard part is making context a later middleware adds (req.user, req.session) visible to the handlers after it, without an `any` cast.
15 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A middleware chain — (req, res, next) => void, called one after another — is easy to type badly and hard to type well. The easy, bad version types every middleware the same, with a fixed Request shape:
type Middleware = (req: Request, res: Response, next: () => void) => void;
const auth: Middleware = (req, res, next) => {
req.user = getUser(req); // ❌ Property 'user' does not exist on type 'Request'
next();
};
The auth middleware's whole job is to add something — req.user — that later middlewares and the final route handler depend on. But Middleware fixes Request as one unchanging shape, so there's nowhere in the type for "and after this one runs, there's also a user field" to live. Reaching for req.user = getUser(req) as any makes the error disappear without fixing the actual gap: nothing downstream now knows user exists either.
The fix is generic middleware, parameterized on what context it requires coming in and what it adds going out:
type Middleware<TIn, TOut> = (
ctx: TIn,
next: (ctx: TOut) => void,
) => void;
const auth: Middleware<{ req: Request }, { req: Request; user: User }> = (
ctx,
next,
) => {
const user = getUser(ctx.req);
next({ ...ctx, user });
};
const logRequest: Middleware<
{ req: Request; user: User },
{ req: Request; user: User }
> = (ctx, next) => {
console.log(`${ctx.user.id} hit ${ctx.req.url}`); // ✅ ctx.user exists, typed as User
next(ctx);
};
auth's output type — { req: Request; user: User } — is logRequest's required input type. The compiler checks that hand-off at the boundary between them: swap the two middlewares' order and logRequest would fail to compile, because ctx.user wouldn't exist yet. The context isn't one fixed shape flowing through unchanged — it's a type that grows, one intersection at a time, and each middleware's signature says exactly what it needs and what it leaves behind for the next one.
any at the point of mutation doesn't just silence one error — it breaks the type for everything downstream. (req as any).user = ... compiles, but every middleware and route handler after it still sees Request with no user, so the unsafety just resurfaces later as a runtime undefined instead of a compile error where it actually happened.req.user = ...) and typing the chain generically are two different strategies that don't mix well. The generic-context version above works because each middleware returns a new context object for next; bolting generics onto in-place mutation of one shared Request object still needs a type assertion somewhere, because the object's declared type never actually changes.logRequest before auth isn't just a logic bug here — it's a compile error, because logRequest's required input type doesn't match what runs before it. That's a real, additional guarantee this pattern buys you over the untyped version.This is the generics lesson's core idea — a type parameter standing in for "whatever gets passed in, filled in per call site" — applied across a sequence of calls instead of one function call. Each middleware's TOut becoming the next one's TIn is exactly the same left-to-right, one-thing-feeds-the-next flow covered in the generic-inference lesson, just with an explicit chain of functions instead of one function's own parameter list.