Перейти к содержимому

48. TypeScript + Express

Привет, яша-лернеры! Сегодня мы углубимся в мир бэкенда и посмотрим, как TypeScript превращает работу с Express из дикой западной истории с any в хорошо организованный, безопасный и предсказуемый симфонический оркестр. Вы уже виртуозно владеете базовыми типами, интерфейсами и дженериками, а значит, готовы поднять свои Express-приложения на новый уровень надежности и удобства разработки.

Express сам по себе — это минималистичный, нетипизированный фреймворк. Он как чистый холст, на котором можно нарисовать что угодно. Но когда ваш проект растет, этот холст может превратиться в запутанный клубок ниток. Где какой тип данных? Что вернет эта функция? Какие поля есть у req.body? Без TypeScript приходится держать все это в голове или постоянно лезть в документацию/исходники.

TypeScript же предоставляет:

  1. Типобезопасность: Защита от распространенных ошибок, связанных с типами, еще на этапе компиляции.
  2. Улучшенное автодополнение: Ваш редактор (VS Code, привет!) будет знать структуру ваших данных, параметров запроса и ответов.
  3. Лучшая читаемость и рефакторинг: Код становится самодокументируемым, а изменения гораздо легче вносить без страха сломать что-то в другом месте.
  4. Четкий API-контракт: Ваши роуты и миддлвары будут явно показывать, что они ожидают и что возвращают.

Прежде чем мы начнем, убедитесь, что у вас установлены Express и его типовые определения:

Окно терминала
npm install express
npm install --save-dev @types/express

Теперь давайте создадим простой сервер:

src/app.ts
import express, { Request, Response, NextFunction } from 'express';
const app = express();
const PORT = 3000;
// Middleware для логирования всех запросов
const loggerMiddleware = (req: Request, res: Response, next: NextFunction) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Передаем управление следующему middleware/роуту
};
app.use(loggerMiddleware);
app.use(express.json()); // Middleware для парсинга JSON тела запросов
// Простой GET роут
app.get('/', (req: Request, res: Response) => {
// TypeScript автоматически понимает типы req и res из пакета @types/express
res.send('Привет, Yasha Learner! Сервер Express с TypeScript работает!');
});
// Роут для демонстрации POST запроса с типизированным телом
interface CreateProductBody {
name: string;
price: number;
description?: string;
}
app.post('/products', (req: Request<{}, {}, CreateProductBody>, res: Response) => {
// req.body теперь имеет тип CreateProductBody!
const { name, price, description } = req.body;
if (!name || typeof price !== 'number') {
return res.status(400).json({ message: 'Имя продукта и цена обязательны и должны быть корректны.' });
}
// В реальном приложении здесь была бы логика сохранения продукта в БД
const newProduct = { id: Date.now().toString(), name, price, description };
res.status(201).json(newProduct);
});
app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});

В этом примере мы видим, как Request, Response и NextFunction импортируются из express. TypeScript сразу же начинает помогать, предоставляя автодополнение для req.method, req.url, res.send и других стандартных методов. Для POST /products мы использовали дженерик Request<{}, {}, CreateProductBody> для явного указания типа тела запроса. Пустые объекты в первых двух аргументах (Params, ResBody) означают, что мы не ожидаем специфичных типов для параметров URL или тела ответа (потому что мы их не определяем явно в дженерике Request).

Очень часто нам нужно добавлять пользовательские свойства в объект req в middleware (например, req.user после аутентификации или req.tenantId в мультитенантных приложениях). Для этого используется объединение объявлений (declaration merging) в TypeScript.

Создайте файл src/types/express.d.ts (или index.d.ts внутри вашей директории src/types), который будет расширять глобальное пространство имен Express:

src/types/express.d.ts
// Важно: этот файл должен быть глобальным модулем.
// Если он содержит import/export верхнего уровня (кроме import { Request } from 'express'),
// TypeScript будет рассматривать его как локальный модуль. Чтобы избежать этого,
// либо убедитесь, что нет export {} на верхнем уровне, либо добавьте в конце файла `export {};`
// для явного объявления его как глобального модуля после использования import.
import { Request } from 'express'; // Импорт здесь - это нормально
declare global {
namespace Express {
// Расширяем интерфейс Request, добавляя наши кастомные поля
interface Request {
user?: {
id: string;
email: string;
roles: string[];
};
tenantId?: string;
}
}
}
// Если у вас есть другие импорты/экспорты на верхнем уровне, добавьте это:
// export {};

Убедитесь, что tsconfig.json включает этот файл:

tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "src/types/**/*.d.ts"], // Убедитесь, что ваш d.ts файл включен
"exclude": ["node_modules"]
}

Теперь, когда Request расширен, давайте используем это в middleware и роуте:

// src/app.ts (продолжение)
// Middleware для аутентификации, который добавляет req.user
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
// В реальном приложении здесь была бы логика проверки токена, сессии и т.д.
const authToken = req.headers.authorization;
if (authToken && authToken === 'Bearer mysecrettoken') {
// Мы добавляем user к req, и TypeScript теперь знает его тип!
req.user = {
id: 'user_123',
roles: ['admin', 'editor'],
};
next();
} else {
res.status(401).json({ message: 'Не авторизован' });
}
};
// Роут, требующий аутентификации
app.get('/profile', authMiddleware, (req: Request, res: Response) => {
// req.user теперь имеет тип { id: string; email: string; roles: string[]; } | undefined
if (req.user) {
res.json({
message: `Добро пожаловать, ${req.user.email}!`,
yourRoles: req.user.roles,
});
} else {
// Этот else блок теоретически недостижим, если authMiddleware работает корректно,
// но TypeScript требует проверки, т.к. req.user опционален.
res.status(401).json({ message: 'Пользователь не найден в запросе.' });
}
});

Обработчики ошибок в Express имеют 4 аргумента: (err, req, res, next). TypeScript предлагает специальный тип ErrorRequestHandler для них:

// src/app.ts (продолжение)
import { ErrorRequestHandler } from 'express'; // Добавьте этот импорт
// Определим кастомный интерфейс для ошибок, которые мы хотим выбрасывать
interface HttpError extends Error {
statusCode?: number;
data?: any;
}
// Типизированный глобальный обработчик ошибок
const errorHandler: ErrorRequestHandler = (err: HttpError, req, res, next) => {
console.error('Обнаружена ошибка:', err);
// Если заголовки ответа уже были отправлены, передаем ошибку дальше
// Это важно, чтобы избежать ошибок "Can't set headers after they are sent to the client"
if (res.headersSent) {
return next(err);
}
const statusCode = err.statusCode || 500;
const message = err.message || 'Внутренняя ошибка сервера';
res.status(statusCode).json({
message: message,
data: err.data, // Например, поля валидации
});
};
// Пример роута, который может выбросить ошибку
app.get('/error-test', (req: Request, res: Response, next: NextFunction) => {
const error: HttpError = new Error('Что-то пошло не так на /error-test!');
error.statusCode = 400;
// Ошибки могут быть переданы в next() для обработки глобальным обработчиком
next(error);
});
// Важно: обработчик ошибок должен быть зарегистрирован ПОСЛЕ всех роутов
// и других middleware, которые могут генерировать ошибки.
app.use(errorHandler);
  1. Забыли @types/express: Error: Cannot find module 'express' or its corresponding type declarations.
    • Решение: npm install --save-dev @types/express.
  2. req.body или req.query имеет тип any: Если вы не указываете типы в дженерике Request или не используете express.json(), TypeScript будет осторожничать.
    • Решение:
      • Используйте app.use(express.json()) и app.use(express.urlencoded({ extended: true })).
      • Явно типизируйте req.body, req.query, req.params с помощью дженериков Request<Params, ResBody, ReqBody, ReqQuery>.
      • Или расширяйте Request через declaration merging для общих свойств.
  3. Неправильное расширение Request: Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
    • Решение: Убедитесь, что ваш express.d.ts находится в корне проекта или включен в tsconfig.json через include, и что он является глобальным модулем (без import/export на верхнем уровне или с export {}; в конце).

Ваш черед, яша-кодер! Укрепим знания:

  1. Создайте middleware для валидации API ключа:
    • Он должен проверять заголовок x-api-key.
    • Если ключ корректный (например, SUPER_SECRET_KEY), он должен добавить свойство req.apiKeyUser: { id: string; name: string; } в объект Request через объявление типов (src/types/express.d.ts).
    • Если ключ отсутствует или некорректен, middleware должен возвращать ошибку 401 с сообщением “Некорректный API ключ”.
    • Создайте роут /data который использует этот middleware и возвращает приветствие Hello, {req.apiKeyUser.name}!.
  2. Реализуйте типизированный роут для поиска товаров:
    • Роут: GET /search-products
    • Принимает параметры запроса (req.query): query: string (обязательный) и category?: string (опциональный).
    • Возвращает массив объектов { id: string; name: string; category: string; }[]. При отсутствии категории, используйте “General”.
    • Верните 400, если query отсутствует, с сообщением “Параметр ‘query’ обязателен”.
  3. Создайте типизированный контроллер:
    • Определите класс UserController.
    • Внутри него создайте статический метод getUsers(req: Request, res: Response) который возвращает статический массив пользователей { id: string; username: string; }[] (например, 2-3 пользователя).
    • Создайте роут app.get('/users', UserController.getUsers).

Всегда стремитесь к максимальной типизации. Чем больше информации TypeScript знает о вашем коде, тем меньше ошибок вы совершите и тем легче будет поддерживать и развивать ваш проект. Для больших проектов с Express рассмотрите использование библиотек, которые изначально спроектированы с учетом TypeScript (например, NestJS или TypeGraphQL), так как они предлагают еще более мощные инструменты для типизации API. Но даже “голый” Express с TypeScript — это уже огромный шаг вперед!