48. TypeScript + Express
TypeScript: Мощный дуэт с Express
Заголовок раздела «TypeScript: Мощный дуэт с Express»Привет, яша-лернеры! Сегодня мы углубимся в мир бэкенда и посмотрим, как TypeScript превращает работу с Express из дикой западной истории с any в хорошо организованный, безопасный и предсказуемый симфонический оркестр. Вы уже виртуозно владеете базовыми типами, интерфейсами и дженериками, а значит, готовы поднять свои Express-приложения на новый уровень надежности и удобства разработки.
🚀 Зачем TypeScript нужен Express?
Заголовок раздела «🚀 Зачем TypeScript нужен Express?»Express сам по себе — это минималистичный, нетипизированный фреймворк. Он как чистый холст, на котором можно нарисовать что угодно. Но когда ваш проект растет, этот холст может превратиться в запутанный клубок ниток. Где какой тип данных? Что вернет эта функция? Какие поля есть у req.body? Без TypeScript приходится держать все это в голове или постоянно лезть в документацию/исходники.
TypeScript же предоставляет:
- Типобезопасность: Защита от распространенных ошибок, связанных с типами, еще на этапе компиляции.
- Улучшенное автодополнение: Ваш редактор (VS Code, привет!) будет знать структуру ваших данных, параметров запроса и ответов.
- Лучшая читаемость и рефакторинг: Код становится самодокументируемым, а изменения гораздо легче вносить без страха сломать что-то в другом месте.
- Четкий API-контракт: Ваши роуты и миддлвары будут явно показывать, что они ожидают и что возвращают.
🛠️ Базовая настройка и типизация
Заголовок раздела «🛠️ Базовая настройка и типизация»Прежде чем мы начнем, убедитесь, что у вас установлены Express и его типовые определения:
npm install expressnpm install --save-dev @types/expressТеперь давайте создадим простой сервер:
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).
✨ Продвинутая типизация: Расширяем Request
Заголовок раздела «✨ Продвинутая типизация: Расширяем Request»Очень часто нам нужно добавлять пользовательские свойства в объект req в middleware (например, req.user после аутентификации или req.tenantId в мультитенантных приложениях). Для этого используется объединение объявлений (declaration merging) в TypeScript.
Создайте файл src/types/express.d.ts (или index.d.ts внутри вашей директории src/types), который будет расширять глобальное пространство имен Express:
// Важно: этот файл должен быть глобальным модулем.// Если он содержит 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 включает этот файл:
{ "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.userconst 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);💡 Типичные ошибки и их решения
Заголовок раздела «💡 Типичные ошибки и их решения»- Забыли
@types/express:Error: Cannot find module 'express' or its corresponding type declarations.- Решение:
npm install --save-dev @types/express.
- Решение:
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 для общих свойств.
- Используйте
- Решение:
- Неправильное расширение
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 {};в конце).
- Решение: Убедитесь, что ваш
🎯 Практика
Заголовок раздела «🎯 Практика»Ваш черед, яша-кодер! Укрепим знания:
- Создайте 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}!.
- Он должен проверять заголовок
- Реализуйте типизированный роут для поиска товаров:
- Роут:
GET /search-products - Принимает параметры запроса (
req.query):query: string(обязательный) иcategory?: string(опциональный). - Возвращает массив объектов
{ id: string; name: string; category: string; }[]. При отсутствии категории, используйте “General”. - Верните 400, если
queryотсутствует, с сообщением “Параметр ‘query’ обязателен”.
- Роут:
- Создайте типизированный контроллер:
- Определите класс
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 — это уже огромный шаг вперед!