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 выдаст ошибку на этапе компиляции. - Проблемы с ци