Avatar ·

Как расширить объект Request в Express с помощью TypeScript

Всем привет! Я пишу бэкенд на Express с TypeScript и столкнулся с проблемой типизации объекта Request. У меня есть middleware, который добавляет в запрос данные о пользователе после аутентификации. Я делаю примерно так: ```typescript import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; interface UserPayload { id: string; email: string; role: 'admin' | 'user'; } export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ message: 'Нет токена' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload; req.user = decoded; // Ошибка TypeScript: Property 'user' does not exist on type 'Request' next(); } catch (error) { return res.status(403).json({ message: 'Неверный токен' }); } }; ``` TypeScript ругается, что у объекта Request нет свойства `user`. Я понимаю, что нужно как-то расширить стандартный тип Request из Express, но не знаю, как это правильно сделать через Declaration Merging. Видел разные варианты в интернете, но хочется понять правильный и современный подход. Подскажите, как правильно объявить типы, чтобы TypeScript знал о новых полях, которые я добавляю в Request? И как сделать так, чтобы это работало во всех файлах проекта, не импортируя дополнительные типы в каждый контроллер? Заранее спасибо! <<>>

Правильный способ расширения Request через Declaration Merging

В Express (и других библиотеках на TypeScript) используется механизм Declaration Merging (слияния объявлений). Это позволяет расширить существующий интерфейс, добавив в него новые свойства. Для Express нужно расширить интерфейс Request из пакета express.

Вот пошаговое решение:

Шаг 1: Создайте файл объявлений типов

Создайте файл types/express.d.ts (или @types/express/index.d.ts) в корне проекта:

// types/express.d.ts
import { Request } from 'express';

// Расширяем глобальный интерфейс Express Request
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        role: 'admin' | 'user';
      };
    }
  }
}

Шаг 2: Настройте tsconfig.json

Убедитесь, что TypeScript видит ваши декларации. Добавьте в tsconfig.json:

{
  "compilerOptions": {
    // ... другие настройки
    "typeRoots": ["./node_modules/@types", "./types"]
  },
  "include": ["src/**/*", "types/**/*"]
}

Шаг 3: Используйте в middleware

Теперь TypeScript знает о свойстве user:

// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ message: 'Нет токена' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as Express.Request['user'];
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(403).json({ message: 'Неверный токен' });
  }
};

Шаг 4: Используйте в контроллерах

Свойство доступно без дополнительных импортов:

// controllers/userController.ts
import { Request, Response } from 'express';

export const getProfile = (req: Request, res: Response) => {
  // req.user — доступен и типизирован!
  if (!req.user) {
    return res.status(401).json({ message: 'Не авторизован' });
  }

  res.json({
    id: req.user.id,
    email: req.user.email,
    role: req.user.role
  });
};

Важные моменты

  • Обязательно используйте declare global — это добавляет типы в глобальную область видимости Express.
  • Сделайте свойство опциональным (user?) — так TypeScript не будет ругаться, если вы не установили его в middleware.
  • Не импортируйте ничего в файле .d.ts — если нужно, используйте синтаксис import() внутри объявления.
  • Для сложных типов вынесите их в отдельный интерфейс и используйте перекрестное объявление (intersection).

Альтернатива: модульное расширение

Если не хотите глобальных типов, можно использовать модульный подход:

// types/express.d.ts
import { Request } from 'express';

export type AuthenticatedRequest = Request & {
  user: {
    id: string;
    email: string;
    role: 'admin' | 'user';
  };
};

Но тогда придётся явно указывать тип AuthenticatedRequest в каждом контроллере, что менее удобно.

Глобальное расширение через Declaration Merging — это стандартный и рекомендуемый способ. Он используется в официальной документации Express и в большинстве реальных проектов.

<<>>

Альтернативный подход: использование модуля express-serve-static-core

Некоторые разработчики предпочитают расширять интерфейс через модуль express-serve-static-core, который является внутренней зависимостью Express. Это может быть полезно, если вы хотите более явно контролировать расширение:

// types/express.d.ts
import 'express-serve-static-core';

declare module 'express-serve-static-core' {
  interface Request {
    user?: {
      id: string;
      email: string;
      role: 'admin' | 'user';
    };
  }
}

Плюсы этого подхода:

  • Не нужно использовать declare global — расширение происходит на уровне модуля
  • Меньше шансов случайно задеть другие глобальные типы
  • Работает одинаково хорошо с последними версиями Express и TypeScript

Минусы:

  • Требует установки @types/express-serve-static-core (обычно идёт в комплекте с @types/express)
  • Может быть менее очевидным для новых разработчиков

Оба подхода валидны и работают. Выбирайте тот, который кажется вам чище и понятнее для вашей команды. Главное — не забудьте настроить typeRoots в tsconfig.json, если ваш файл деклараций лежит не в стандартной папке.