O middleware valida o token e coloca um usuário em req.user. A rota seguinte recebe o mesmo objeto em runtime, mas o TypeScript continua reclamando que a propriedade não existe ou pode ser undefined.
Isso não é uma falha do compilador. O Express executa funções em uma ordem configurada no runtime. O TypeScript verifica cada handler sem provar qual caminho de middleware chegou até ele. A solução segura combina um tipo global opcional com uma verificação que torna a garantia explícita na rota.
Este texto usa req.user como exemplo, mas a mesma fronteira vale para req.locale, req.requestId e outros dados de contexto. Para organizar a lógica que fica entre request, serviço e resposta, veja como organizar fronteiras entre serviços TypeScript.

Resposta curta
- Declare propriedades adicionadas ao request como opcionais.
- Valide o valor antes de usá-lo em uma rota protegida.
- Use um tipo local, uma função de asserção ou
res.localspara expressar a garantia.- Teste também a rota sem o middleware. O tipo não substitui a proteção em runtime.
Por que o TypeScript não acompanha a ordem do middleware?
O Express permite que um middleware mude o request, o response, encerre o ciclo ou chame next(). A documentação oficial, em "Writing middleware for use in Express apps", também destaca que a ordem de carregamento importa: funções registradas antes são executadas antes (Express, "Writing middleware for use in Express apps").
Essa ordem pertence à composição da aplicação. Ela pode mudar quando uma rota ganha outro prefixo, quando um router é montado em outro lugar ou quando alguém reaproveita o handler sem o middleware de autenticação. Uma assinatura que marque user como sempre presente faria uma promessa para todas essas chamadas.
Por isso, a pergunta correta não é "como faço o TypeScript confiar no meu middleware?". É "em qual ponto da aplicação eu consigo provar que o valor existe?". Essa mudança de pergunta evita dois atalhos perigosos: usar as any e tornar user obrigatório em todos os requests.
Como declarar a propriedade adicionada ao request?
Use declaration merging em um arquivo .d.ts incluído pelo projeto. A documentação do Express, em "Overriding the Express API", usa esse padrão para descrever user? no namespace global Express (Express, "Overriding the Express API").
O exemplo abaixo é ilustrativo. O projeto real precisa escolher o tipo de usuário e confirmar que o arquivo está dentro do include do tsconfig.json:
// src/types/express.d.ts
export {};
type User = {
id: string;
role: "admin" | "member";
};
declare global {
namespace Express {
interface Request {
user?: User;
}
}
}
A propriedade é opcional de propósito. Uma rota pública pode passar por outro conjunto de funções. O próprio Express alerta que o TypeScript não sabe qual middleware rodou antes de um handler, portanto um campo obrigatório seria uma garantia falsa (Express, "Overriding the Express API").
Esse arquivo descreve o contrato possível. Ele não cria req.user em runtime, não valida um token e não impede uma rota de esquecer o middleware. Declaration merging apenas reúne declarações de interfaces com o mesmo nome no tipo que o compilador consulta, como explica o handbook do TypeScript (TypeScript, "Declaration Merging").
Como validar o dado antes de chegar ao handler?
Faça a checagem no limite da rota. Um middleware de autenticação deve encerrar a requisição quando não encontra o usuário e chamar next() somente depois da validação:
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();
}
A checagem acima protege o comportamento da aplicação, mas ainda não muda a assinatura de todo handler que o Express recebe. Dentro de um handler protegido, uma função de asserção pode transformar a verificação em uma garantia 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");
}
}
O retorno asserts req is ... é uma asserção de tipo. Depois que a função termina sem lançar erro, o TypeScript estreita o tipo de req naquele fluxo. O handbook do TypeScript, em "Narrowing", documenta esse uso de predicados e análise de fluxo (TypeScript, "Narrowing").
No handler, a fronteira fica visível:
app.get("/me", requireUser, (req, res) => {
assertUser(req);
res.json({
id: req.user.id,
role: req.user.role,
});
});
O exemplo continua precisando de tratamento de erro adequado para o seu projeto. A asserção não é um passe livre para ignorar autenticação. Ela repete a prova na fronteira do consumidor para que o código permaneça correto mesmo se alguém reutilizar o handler em outra composição.
Quando res.locals é uma escolha melhor?
Use res.locals quando o dado pertence ao ciclo daquela resposta e não precisa parecer uma propriedade universal do request. O Express mantém res.locals disponível durante um único ciclo request-response, sem compartilhar o valor entre requests (Express, "Response").
Essa opção deixa o contexto produzido pelo middleware separado da entrada recebida pelo 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 });
}
A tipagem genérica precisa acompanhar a versão dos tipos do Express instalada no projeto. Se a definição local não aceitar esses parâmetros, mantenha a mesma ideia com um tipo de handler nomeado e verifique o resultado usando tsc --noEmit. O ponto não é decorar a assinatura. É impedir que o consumidor use o valor sem passar pela fronteira.
Como escolher entre request, res.locals e um tipo local?
Use a propriedade no request quando vários middlewares precisam ler o mesmo contexto durante a requisição. Use res.locals quando o valor nasce para a resposta ou para os handlers de uma cadeia específica. Use um tipo local quando somente uma parte da aplicação pode receber a garantia.
| Situação | Escolha | Motivo |
|---|---|---|
| O valor pode existir em qualquer request, mas não em todos | propriedade opcional em Request |
representa a possibilidade sem prometer presença |
| O valor foi carregado para uma cadeia de handlers | res.locals tipado |
limita o contexto ao ciclo da resposta |
| Uma rota exige o valor | asserção ou handler local | torna a pré-condição explícita no ponto de uso |
| O valor é criado por uma biblioteca externa | augmentation no arquivo de tipos | descreve o contrato que o código já espera |
Não use declaration merging para apagar a diferença entre rota pública e rota protegida. A documentação do Express permite estender Request, mas recomenda o campo opcional justamente porque o compilador não conhece a composição anterior. O tipo deve acompanhar o alcance real da garantia.
Essa mesma separação entre dados aceitos e efeitos permitidos aparece ao validar dados antes de executar uma ação: primeiro estabeleça a pré-condição, depois deixe o handler operar com um contrato menor.
Como verificar se a tipagem está realmente protegendo a rota?
Comece verificando o compilador, não o editor. Execute npx tsc --noEmit e confirme duas coisas: uma chave inexistente falha e o handler protegido consegue acessar req.user.id depois da asserção. O código deste artigo é ilustrativo, então ajuste imports e genéricos ao projeto antes de executar.
Depois teste o comportamento. O caso sem credencial deve retornar a resposta de não autorizado. O caso com credencial deve chamar next() e chegar ao handler com o usuário esperado. Também teste uma composição em que o handler é montado sem requireUser; essa combinação precisa falhar de forma controlada, não apenas produzir um erro de TypeScript.
Para o caminho HTTP, use um teste que atravesse a aplicação e confira a resposta real. A separação entre mock e comportamento integrado importa: veja como testar o caminho real sem falsos positivos. Se a aplicação é dividida em projetos, declarar fronteiras entre projetos TypeScript também ajuda a impedir que o arquivo de tipos fique fora do build.
O que costuma dar errado?
O primeiro erro é declarar user: User globalmente. Isso silencia o compilador em rotas públicas, mas cria um tipo que mente sobre o runtime. Mantenha user? e prove a presença apenas onde a rota realmente exige o valor.
O segundo é usar req.user! em todos os handlers. O operador de não nulo pode ser aceitável depois de uma prova curta, mas espalhá-lo pela aplicação esconde a pré-condição. Uma função de asserção centraliza a checagem e fornece um lugar único para ajustar a resposta ou o erro.
O terceiro é criar o arquivo de augmentation e esquecer o tsconfig.json. Se o editor conhece o arquivo, mas o CI não, compare o include, o exclude e a lista de arquivos carregados pelo compilador. O Express também chama atenção para esse detalhe quando diz que nenhuma mudança de configuração é necessária, exceto quando um include personalizado deixa o arquivo de fora (Express, "Writing middleware for use in Express apps").
O quarto é confiar no tipo sem testar a composição. TypeScript descreve o programa que você declarou. Ele não observa uma ordem de app.use em produção nem sabe se uma rota foi exportada sem o middleware. A proteção precisa existir nas duas camadas.
Perguntas frequentes
Declaration merging deixa req.user obrigatório?
Não. O padrão seguro é declarar user?, porque a interface vale para todas as requisições. O Express recomenda a propriedade opcional e uma checagem antes do uso. Uma rota protegida pode estreitar o tipo com uma asserção ou um handler local, mas isso não deve mudar a promessa feita para as rotas públicas.
Devo colocar o usuário em req ou em res.locals?
Depende do alcance do dado. req funciona quando o contexto será lido por várias partes da cadeia. res.locals é útil quando o valor pertence ao ciclo da resposta e deve ser compartilhado entre handlers daquela cadeia. Em ambos os casos, o middleware ainda precisa validar o valor em runtime.
Por que um cast não resolve a ordem do middleware?
Um cast altera o que o compilador aceita naquele ponto. Ele não valida token, não adiciona uma propriedade e não muda a ordem de execução. Use um cast somente depois de uma prova concreta, preferindo uma função de asserção que reúna a checagem e a mensagem de erro.
Conclusão
Tipar dados adicionados por middleware no Express exige separar três coisas: o que qualquer request pode ter, o que o middleware realmente valida e o que uma rota específica está autorizada a usar.
Declare o campo como opcional, faça a checagem no runtime e modele a garantia perto do consumidor. Essa combinação conserva a flexibilidade do Express sem deixar o TypeScript aceitar uma promessa que a aplicação não consegue cumprir.
Fontes consultadas
- Express, "Writing middleware for use in Express apps", consultado em 31/08/2026.
- Express, "Overriding the Express API", consultado em 31/08/2026.
- Express, "Response", consultado em 31/08/2026.
- TypeScript, "Declaration Merging", consultado em 31/08/2026.
- TypeScript, "Narrowing", consultado em 31/08/2026.