Typegoose для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Typegoose для TypeScript: Полное руководство

Typegoose — это мощная библиотека-обёртка для Mongoose, которая позволяет определять модели MongoDB с использованием классов и декораторов TypeScript. Она решает ключевую проблему разработки на Node.js: несоответствие между строгой типизацией TypeScript и динамической природой Mongoose. Вместо того чтобы вручную дублировать типы и схемы, Typegoose позволяет описать модель один раз — как класс TypeScript, и автоматически генерирует из него Mongoose-схему.

Зачем это нужно? Во-первых, это значительно сокращает количество шаблонного кода. Вам больше не нужно писать отдельный интерфейс и отдельную схему — класс TypeScript выполняет обе функции. Во-вторых, Typegoose обеспечивает полную безопасность типов на всех этапах: от создания документа до выполнения запросов. IDE будет подсказывать поля, методы и их типы, что практически исключает ошибки, связанные с опечатками в названиях полей или неверными типами данных. В-третьих, библиотека поддерживает все возможности Mongoose: виртуальные поля, хуки (pre/post), индексы, populate и middleware.

Typegoose идеально подходит для крупных проектов, где важна поддерживаемость кода, а также для команд, которые хотят использовать все преимущества ООП в TypeScript. Он легко интегрируется с NestJS, Express и другими фреймворками, предоставляя единый стандарт описания данных.

Установка

Для установки Typegoose и его зависимостей выполните в терминале:

npm install @typegoose/typegoose mongoose
npm install -D @types/mongoose

Если вы используете yarn:

yarn add @typegoose/typegoose mongoose
yarn add -D @types/mongoose

Для работы Typegoose требуется TypeScript версии 4.0 или выше. Убедитесь, что в вашем tsconfig.json включены следующие опции:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2020",
    "module": "commonjs"
  }
}

Быстрый старт

Создадим простую модель пользователя и сохраним её в MongoDB.

import { prop, getModelForClass } from '@typegoose/typegoose';
import mongoose from 'mongoose';

// 1. Определяем класс модели
class User {
  @prop({ required: true })
  public name!: string;

  @prop({ required: true, unique: true })
  public email!: string;

  @prop({ default: 0 })
  public age!: number;
}

// 2. Получаем Mongoose-модель
const UserModel = getModelForClass(User);

// 3. Подключаемся к БД и работаем
async function main() {
  await mongoose.connect('mongodb://localhost:27017/test');

  // Создаём пользователя
  const user = await UserModel.create({
    name: 'Alice',
    email: 'alice@example.com',
    age: 30,
  });

  console.log('Created:', user);

  // Находим пользователя
  const found = await UserModel.findOne({ email: 'alice@example.com' });
  console.log('Found:', found);
}

main().catch(console.error);

Этот минимальный пример демонстрирует основную идею: класс User с декоратором @prop описывает схему, а функция getModelForClass создаёт готовую к использованию Mongoose-модель.

Основные методы и функции

1. @prop() — декоратор свойства

Сигнатура: @prop(options?: PropOptions)

Описание: Определяет поле в Mongoose-схеме. Принимает объект опций, аналогичный опциям полей в Mongoose (required, default, unique, ref, enum и т.д.).

import { prop } from '@typegoose/typegoose';

class Product {
  @prop({ required: true, type: String })
  public title!: string;

  @prop({ default: 0, min: 0 })
  public price!: number;

  @prop({ enum: ['active', 'inactive'], default: 'active' })
  public status!: string;
}

2. getModelForClass() — создание модели

Сигнатура: getModelForClass<T>(cls: ClassType<T>, options?: ModelOptions): ModelType<T>

Описание: Преобразует класс TypeScript в полноценную Mongoose-модель. Возвращает типизированную модель с методами Mongoose (find, create, updateOne и т.д.).

import { getModelForClass } from '@typegoose/typegoose';

class Cat {
  @prop()
  public name!: string;
}

const CatModel = getModelForClass(Cat);
// CatModel теперь имеет все методы Mongoose
const cats = await CatModel.find({ name: 'Whiskers' });

3. @modelOptions() — настройка модели

Сигнатура: @modelOptions(options: ModelOptions)

Описание: Позволяет задать глобальные опции для схемы, такие как имя коллекции, timestamps, toJSON, toObject и другие.

import { modelOptions } from '@typegoose/typegoose';

@modelOptions({
  schemaOptions: {
    timestamps: true, // добавляет createdAt и updatedAt
    collection: 'users_custom', // имя коллекции
  },
})
class User {
  @prop()
  public name!: string;
}

4. @index() — создание индекса

Сигнатура: @index(fields: IndexDefinition, options?: IndexOptions)

Описание: Определяет составной или уникальный индекс для схемы. Можно применять как к классу, так и к отдельным полям.

import { index, prop } from '@typegoose/typegoose';

@index({ email: 1 }, { unique: true })
@index({ name: 1, age: -1 })
class Person {
  @prop()
  public name!: string;

  @prop()
  public email!: string;

  @prop()
  public age!: number;
}

5. @pre() и @post() — хуки жизненного цикла

Сигнатура: @pre<T>(hook: string, fn: PreSaveFunction<T>) и @post<T>(hook: string, fn: PostSaveFunction<T>)

Описание: Позволяют выполнять код до (pre) или после (post) определённых событий Mongoose (save, validate, findOneAndUpdate и т.д.).

import { pre, prop } from '@typegoose/typegoose';

@pre<User>('save', function () {
  this.updatedAt = new Date();
})
class User {
  @prop()
  public name!: string;

  @prop()
  public updatedAt!: Date;
}

6. @plugin() — подключение плагинов Mongoose

Сигнатура: @plugin(plugin: PluginFunction, options?: object)

Описание: Подключает сторонние Mongoose-плагины (например, mongoose-paginate, mongoose-aggregate-paginate) к модели.

import { plugin, prop } from '@typegoose/typegoose';
import * as mongoosePaginate from 'mongoose-paginate';

@plugin(mongoosePaginate)
class Post {
  @prop()
  public title!: string;

  @prop()
  public content!: string;
}

7. @virtual() — виртуальные поля

Сигнатура: @virtual(options?: VirtualOptions)

Описание: Определяет виртуальное поле, которое не хранится в БД, но вычисляется на основе других полей. Поддерживает getter и setter.

import { virtual, prop } from '@typegoose/typegoose';

class User {
  @prop()
  public firstName!: string;

  @prop()
  public lastName!: string;

  @virtual()
  public get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

8. @arrayProp() — массивы (устаревший, но встречается)

Сигнатура: @arrayProp(items: ArrayPropOptions)

Описание: Определяет поле-массив. В текущих версиях Typegoose рекомендуется использовать @prop({ type: () => [SubType] }).

import { prop } from '@typegoose/typegoose';

class Blog {
  @prop({ type: () => [String] })
  public tags!: string[];

  @prop({ type: () => [Comment] })
  public comments!: Comment[];
}

class Comment {
  @prop()
  public text!: string;
}

Реальный практический пример

Создадим полноценный REST API для управления задачами (Todo list) с использованием Typegoose и Express.

import express from 'express';
import mongoose from 'mongoose';
import { prop, getModelForClass, modelOptions, index } from '@typegoose/typegoose';

// 1. Определяем модель задачи
@modelOptions({ schemaOptions: { timestamps: true } })
@index({ status: 1, createdAt: -1 })
class Task {
  @prop({ required: true, trim: true, maxlength: 200 })
  public title!: string;

  @prop({ maxlength: 1000 })
  public description?: string;

  @prop({ enum: ['todo', 'in_progress', 'done'], default: 'todo' })
  public status!: string;

  @prop({ default: 1, min: 1, max: 5 })
  public priority!: number;

  @prop({ default: false })
  public isArchived!: boolean;
}

const TaskModel = getModelForClass(Task);

// 2. Настраиваем Express
const app = express();
app.use(express.json());

// 3. CRUD операции
app.get('/tasks', async (req, res) => {
  try {
    const { status, priority, page = '1', limit = '10' } = req.query;
    const filter: any = { isArchived: false };

    if (status) filter.status = status;
    if (priority) filter.priority = Number(priority);

    const tasks = await TaskModel.find(filter)
      .sort({ createdAt: -1 })
      .skip((Number(page) - 1) * Number(limit))
      .limit(Number(limit));

    const total = await TaskModel.countDocuments(filter);

    res.json({
      data: tasks,
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total,
        pages: Math.ceil(total / Number(limit)),
      },
    });
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch tasks' });
  }
});

app.post('/tasks', async (req, res) => {
  try {
    const task = await TaskModel.create(req.body);
    res.status(201).json(task);
  } catch (error: any) {
    if (error.name === 'ValidationError') {
      return res.status(400).json({ error: error.message });
    }
    res.status(500).json({ error: 'Failed to create task' });
  }
});

app.patch('/tasks/:id', async (req, res) => {
  try {
    const task = await TaskModel.findByIdAndUpdate(
      req.params.id,
      { $set: req.body },
      { new: true, runValidators: true }
    );
    if (!task) return res.status(404).json({ error: 'Task not found' });
    res.json(task);
  } catch (error) {
    res.status(500).json({ error: 'Failed to update task' });
  }
});

app.delete('/tasks/:id', async (req, res) => {
  try {
    const task = await TaskModel.findByIdAndDelete(req.params.id);
    if (!task) return res.status(404).json({ error: 'Task not found' });
    res.json({ message: 'Task deleted' });
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete task' });
  }
});

// 4. Запуск
async function start() {
  await mongoose.connect('mongodb://localhost:27017/todo_app');
  console.log('Connected to MongoDB');

  app.listen(3000, () => {
    console.log('Server running on port 3000');
  });
}

start().catch(console.error);

Этот пример демонстрирует все основные возможности Typegoose: валидацию полей, индексы, timestamps, а также интеграцию с Express для построения полноценного API.

Советы по производительности и типичные ошибки

Советы по производительности

  • Используйте lean() для чтения: При запросах, где не нужны методы Mongoose (например, для простого вывода данных), используйте .lean(). Это возвращает простые JavaScript-объекты и значительно ускоряет запросы.
  • Создавайте индексы: Всегда добавляйте индексы для полей, по которым часто выполняются запросы. Используйте декоратор @index для составных индексов.
  • Избегайте глубокой вложенности: MongoDB лучше работает с плоскими структурами. Если вам нужно хранить вложенные данные, рассмотрите возможность нормализации.
  • Используйте проекцию: В методах find, findOne указывайте только нужные поля: UserModel.find({}, 'name email').
  • Пул соединений: Настройте пул соединений Mongoose через опцию poolSize (по умолчанию 100). Для высоконагруженных приложений увеличьте до 200-300.

Типичные ошибки

  • Забыли experimentalDecorators: Если Typegoose не работает, проверьте tsconfig.json. Без "experimentalDecorators": true и "emitDecoratorMetadata": true декораторы не будут работать.
  • Несоответствие типов: Typegoose строго проверяет типы. Если вы определили поле как string, но передаёте number, TypeScript выдаст ошибку на этапе компиляции.
  • Проблемы с ци

Рекомендаций