TypeScript с Node.js
В этой главе показано, как настроить проект Node.js с TypeScript, создать небольшой сервер на Express, добавить типизированное промежуточное ПО, подключить базу данных через TypeORM и организовать повседневный рабочий процесс разработки.
Зачем использовать TypeScript вместе с Node.js?
TypeScript добавляет статическую типизацию в разработку на Node.js. Это означает более удобные инструменты, более высокое качество кода и более приятную работу разработчика.
Основные преимущества:
- Безопасность типов для кода на JavaScript
- Лучшая поддержка в IDE с автодополнением
- Ошибки находятся рано, ещё во время написания кода
- Код проще сопровождать, и он читается как документация
- Рефакторинг выполняется проще
Требования: установите свежую LTS-версию Node.js (рекомендуется v18 или новее) и npm. Проверьте командами node -v и npm -v.
Настройка проекта Node.js с TypeScript
В этом разделе шаг за шагом создаётся новый проект Node.js, настроенный для TypeScript.
Примечание: во время разработки вы пишете код на TypeScript (.ts), а затем компилируете его в JavaScript (.js), чтобы Node.js мог запускать его в продакшене.
1. Создайте новый проект. Выполните эти команды в терминале:
mkdir my-ts-node-appcd my-ts-node-appnpm init -ynpm install typescript @types/node --save-devnpx tsc --init
Что они делают: typescript добавляет компилятор TypeScript (tsc), @types/node предоставляет определения типов для Node.js, а npx tsc --init создаёт файл конфигурации tsconfig.json.
2. Создайте папку с исходным кодом. Держите исходный код в src/, а результат компиляции — в dist/. Выполните mkdir src; позже вы добавите файлы, такие как src/server.ts и src/middleware/auth.ts.
3. Настройте TypeScript. Отредактируйте созданный tsconfig.json так, чтобы compilerOptions содержал:
"target": "ES2020""module": "commonjs""outDir": "./dist""rootDir": "./src""strict": true"esModuleInterop": true"skipLibCheck": true"forceConsistentCasingInFileNames": true"moduleResolution": "node""resolveJsonModule": true"sourceMap": true
а рядом с ним — "include": ["src/**/*"] и "exclude": ["node_modules"].
Главное про параметры: rootDir и outDir разделяют исходный код (src) и результат сборки (dist); strict включает самую строгую проверку типов; esModuleInterop позволяет модулям CommonJS и ES работать вместе более гладко; sourceMap создаёт карты исходников для отладки скомпилированного кода.
CommonJS против ESM: в этом руководстве используется module: "commonjs". Если вы используете ESM (type: "module" в package.json), задайте module: "nodenext" или node16 и последовательно применяйте import/export.
4. Установите зависимости для выполнения и для разработки. Установите Express для обработки HTTP, а также несколько полезных инструментов разработчика:
npm install express body-parsernpm install --save-dev ts-node nodemon @types/express
Предупреждение: используйте ts-node и nodemon только во время разработки. Для продакшена скомпилируйте код с помощью tsc и запускайте Node на полученном JavaScript.
Структура проекта. Держите проект в порядке: папка my-ts-node-app/ содержит src/ (с файлами server.ts, middleware/auth.ts, entity/User.ts и config/database.ts), созданную папку dist/, node_modules/, package.json и tsconfig.json.
Пример простого сервера на TypeScript
Этот пример показывает минимальный сервер Express, написанный на TypeScript. В нём есть типизированная модель User и несколько маршрутов.
Что здесь добавляет TypeScript:
- Типизированные
Request,ResponseиNextFunctionдля обработчиков Express. - Интерфейс
User, который гарантирует форму данных пользователя. - Более безопасный рефакторинг и лучшее автодополнение для параметров маршрута и тел запросов.
Минимальный сервер на Express
Список пользователей в памяти, отдаваемый через три типизированных маршрута, с промежуточным ПО для обработки ошибок в конце.
import express, { Request, Response, NextFunction } from 'express';
import { json } from 'body-parser';
interface User {
id: number;
username: string;
email: string;
}
// Initialize Express app
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(json());
// In-memory database
const users: User[] = [
{ id: 1, username: 'user1', email: '[email protected]' },
{ id: 2, username: 'user2', email: '[email protected]' }
];
// Routes
app.get('/api/users', (req: Request, res: Response) => {
res.json(users);
});
app.get('/api/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
});
app.post('/api/users', (req: Request, res: Response) => {
const { username, email } = req.body;
if (!username || !email) {
return res.status(400).json({ message: 'Username and email are required' });
}
const newUser: User = {
id: users.length + 1,
username,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong!' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Использование TypeScript с промежуточным ПО Express
Промежуточное ПО тоже можно строго типизировать.
Вы также можете расширить типы Express с помощью слияния объявлений, чтобы объект запроса мог нести данные аутентифицированного пользователя.
Типизированное промежуточное ПО аутентификации
authenticate читает bearer-токен и сохраняет пользователя в запросе; authorize проверяет роль пользователя.
import { Request, Response, NextFunction } from 'express';
// Extend the Express Request type to include custom properties
declare global {
namespace Express {
interface Request {
user?: { id: number; role: string };
}
}
}
export const authenticate = (req: Request, res: Response, next: NextFunction) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'No token provided' });
}
try {
// In a real app, verify the JWT token here
const decoded = { id: 1, role: 'admin' }; // Mock decoded token
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ message: 'Invalid token' });
}
};
export const authorize = (roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Not authorized' });
}
next();
};
};
Использование промежуточного ПО в маршруте
В файле src/server.ts функции промежуточного ПО выстраиваются в цепочку перед обработчиком администратора.
// src/server.ts
import { authenticate, authorize } from './middleware/auth';
app.get('/api/admin', authenticate, authorize(['admin']), (req, res) => {
res.json({ message: `Hello admin ${req.user?.id}` });
});
TypeScript с базой данных (пример TypeORM)
Вы можете использовать ORM, например TypeORM, который применяет декораторы TypeScript, чтобы сопоставлять классы с таблицами базы данных.
Прежде чем начать:
- Установите пакеты:
npm install typeorm reflect-metadata pg(используйтеpgдля PostgreSQL). - Поскольку применяются декораторы, включите
"experimentalDecorators": trueи"emitDecoratorMetadata": trueвcompilerOptionsфайлаtsconfig.json. - Импортируйте
reflect-metadataодин раз при запуске приложения.
Сущность User с декораторами
Каждый декоратор сообщает TypeORM, как свойство класса сопоставляется со столбцом таблицы users.
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@Column({ default: 'user' })
role: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Настройка источника данных
DataSource читает параметры подключения из переменных окружения, со значениями по умолчанию для локальной работы.
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from '../entity/User';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'mydb',
synchronize: process.env.NODE_ENV !== 'production',
logging: false,
entities: [User],
migrations: [],
subscribers: [],
});
Запуск сервера после подключения к базе данных
В файле src/server.ts сервер начинает слушать запросы только после того, как источник данных инициализирован.
// src/server.ts
import { AppDataSource } from './config/database';
AppDataSource.initialize()
.then(() => {
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
})
.catch((err) => {
console.error('DB init error', err);
process.exit(1);
});
Рабочий процесс разработки
1. Добавьте скрипты в package.json. Поместите их в раздел "scripts":
"build": "tsc""start": "node dist/server.js""dev": "nodemon --exec ts-node src/server.ts""watch": "tsc -w""test": "jest --config jest.config.js"
Примечание: скрипт test необязателен и предполагает, что настроен Jest. Если вы не используете Jest, не добавляйте его.
2. Запустите в режиме разработки командой npm run dev.
3. Соберите для продакшена командой npm run build, а затем запустите её результат командой npm start.
Отладка с картами исходников. Когда в tsconfig.json включён sourceMap, вы можете отлаживать скомпилированный код и при этом видеть свои файлы .ts: выполните node --enable-source-maps dist/server.js.
Совет: большинство IDE, включая VS Code, поддерживают отладку TypeScript с точками останова, когда включены карты исходников.
Лучшие практики
- Всегда указывайте тип параметров функции и возвращаемого значения
- Используйте интерфейсы для описания формы объектов
- Включайте строгий режим в
tsconfig.json - Используйте защитники типов для проверок типа во время выполнения
- Используйте служебные типы TypeScript (
Partial,Pick,Omitи так далее) - Храните общие определения типов в файлах
.d.ts - Используйте перечисления или const-утверждения для фиксированных наборов значений
- Документируйте сложные типы комментариями JSDoc
- Предпочитайте переменные окружения для секретов и конфигурации и проверяйте их при запуске
- Используйте
ts-node/nodemonтолько при разработке; для продакшена компилируйте код - Рассмотрите ESLint + Prettier с
@typescript-eslintдля стабильного качества кода