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.

Diagrama mostra um middleware adicionando req.user, uma checagem em runtime e um handler tipado no Express.

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.locals para 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