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

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-app
  • cd my-ts-node-app
  • npm init -y
  • npm install typescript @types/node --save-dev
  • npx 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-parser
  • npm 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

Список пользователей в памяти, отдаваемый через три типизированных маршрута, с промежуточным ПО для обработки ошибок в конце.

script.ts
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 проверяет роль пользователя.

script.ts
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 функции промежуточного ПО выстраиваются в цепочку перед обработчиком администратора.

script.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.

script.ts
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 читает параметры подключения из переменных окружения, со значениями по умолчанию для локальной работы.

script.ts
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 сервер начинает слушать запросы только после того, как источник данных инициализирован.

script.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 для стабильного качества кода