Попытка реализовать модель Mongoose в Typescript. Поиск в Google выявил только гибридный подход (объединение JS и TS). Как при моем довольно наивном подходе реализовать класс User без JS?
Хотите иметь возможность IUserModel без багажа.
import {IUser} from './user.ts';
import {Document, Schema, Model} from 'mongoose';
// mixing in a couple of interfaces
interface IUserDocument extends IUser, Document {}
// mongoose, why oh why '[String]'
// TODO: investigate out why mongoose needs its own data types
let userSchema: Schema = new Schema({
userName : String,
password : String,
firstName : String,
lastName : String,
email : String,
activated : Boolean,
roles : [String]
});
// interface we want to code to?
export interface IUserModel extends Model<IUserDocument> {/* any custom methods here */}
// stumped here
export class User {
constructor() {}
}
javascript
node.js
mongoose
typescript
Тим Макнамара
источник
источник
User
не может быть классом, потому что его создание - асинхронная операция. Он должен вернуть обещание, поэтому вы должны позвонитьUser.create({...}).then...
.User
не может быть классом?Ответы:
Вот как я это делаю:
export interface IUser extends mongoose.Document { name: string; somethingElse?: number; }; export const UserSchema = new mongoose.Schema({ name: {type:String, required: true}, somethingElse: Number, }); const User = mongoose.model<IUser>('User', UserSchema); export default User;
источник
import * as mongoose from 'mongoose';
илиimport mongoose = require('mongoose');
import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
let newUser = new User({ iAmNotHere: true })
без ошибок в IDE или при компиляции. Так в чем же причина создания интерфейса?Другой вариант, если вы хотите отделить определения типов от реализации базы данных.
import {IUser} from './user.ts'; import * as mongoose from 'mongoose'; type UserType = IUser & mongoose.Document; const User = mongoose.model<UserType>('User', new mongoose.Schema({ userName : String, password : String, /* etc */ }));
Вдохновение отсюда: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models
источник
mongoose.Schema
Дублирует ли определение здесь поля изIUser
? Учитывая , чтоIUser
определен в другом файле с риском того, что поля с рассинхронизироваться как проект растет в сложности и количества разработчиков, достаточно высока.Извините за некропостинг, но это может быть кому-то интересно. Я думаю, что Typegoose предоставляет более современный и элегантный способ определения моделей.
Вот пример из документации:
import { prop, Typegoose, ModelType, InstanceType } from 'typegoose'; import * as mongoose from 'mongoose'; mongoose.connect('mongodb://localhost:27017/test'); class User extends Typegoose { @prop() name?: string; } const UserModel = new User().getModelForClass(User); // UserModel is a regular Mongoose Model with correct types (async () => { const u = new UserModel({ name: 'JohnDoe' }); await u.save(); const user = await UserModel.findOne(); // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 } console.log(user); })();
Для существующего сценария подключения вы можете использовать следующее (что может быть более вероятно в реальных ситуациях и раскрыто в документации):
import { prop, Typegoose, ModelType, InstanceType } from 'typegoose'; import * as mongoose from 'mongoose'; const conn = mongoose.createConnection('mongodb://localhost:27017/test'); class User extends Typegoose { @prop() name?: string; } // Notice that the collection name will be 'users': const UserModel = new User().getModelForClass(User, {existingConnection: conn}); // UserModel is a regular Mongoose Model with correct types (async () => { const u = new UserModel({ name: 'JohnDoe' }); await u.save(); const user = await UserModel.findOne(); // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 } console.log(user); })();
источник
typegoose
меня беспокоит, что поддержки не хватает ... проверяя их статистику npm, это всего 3 тыс. Загрузок в неделю, а на Github почти 100 открытых проблем, большинство из которых без комментариев, и некоторые из них, похоже, должны были быть закрыты давным-давноtypegoose
- мы закончили тем, что вручную обработали наш набор текста, похоже на этот пост , похоже,ts-mongoose
может быть какое-то обещание (как предлагается в последующем ответе)Попробуй
ts-mongoose
. Для сопоставления используются условные типы.import { createSchema, Type, typedModel } from 'ts-mongoose'; const UserSchema = createSchema({ username: Type.string(), email: Type.string(), }); const User = typedModel('User', UserSchema);
источник
Большинство ответов здесь повторяют поля в классе / интерфейсе TypeScript и в схеме мангуста. Отсутствие единого источника истины представляет собой риск обслуживания, поскольку проект становится все более сложным и над ним работают все больше разработчиков: поля с большей вероятностью рассинхронизируются . Это особенно плохо, когда класс находится в другом файле, а не в схеме мангуста.
Чтобы поля синхронизировались, имеет смысл определить их один раз. Для этого есть несколько библиотек:
Я еще не был полностью убежден ни в одном из них, но typegoose, похоже, активно поддерживается, и разработчик принял мои PR.
Чтобы подумать на шаг впереди: когда вы добавляете схему GraphQL в микс, появляется еще один уровень дублирования модели. Одним из способов решения этой проблемы может быть создание кода TypeScript и мангуста из схемы GraphQL.
источник
Вот строгий типизированный способ сопоставления простой модели со схемой мангуста. Компилятор гарантирует, что определения, переданные в mongoose.Schema, соответствуют интерфейсу. Когда у вас есть схема, вы можете использовать
common.ts
export type IsRequired<T> = undefined extends T ? false : true; export type FieldType<T> = T extends number ? typeof Number : T extends string ? typeof String : Object; export type Field<T> = { type: FieldType<T>, required: IsRequired<T>, enum?: Array<T> }; export type ModelDefinition<M> = { [P in keyof M]-?: M[P] extends Array<infer U> ? Array<Field<U>> : Field<M[P]> };
user.ts
import * as mongoose from 'mongoose'; import { ModelDefinition } from "./common"; interface User { userName : string, password : string, firstName : string, lastName : string, email : string, activated : boolean, roles : Array<string> } // The typings above expect the more verbose type definitions, // but this has the benefit of being able to match required // and optional fields with the corresponding definition. // TBD: There may be a way to support both types. const definition: ModelDefinition<User> = { userName : { type: String, required: true }, password : { type: String, required: true }, firstName : { type: String, required: true }, lastName : { type: String, required: true }, email : { type: String, required: true }, activated : { type: Boolean, required: true }, roles : [ { type: String, required: true } ] }; const schema = new mongoose.Schema( definition );
Когда у вас есть схема, вы можете использовать методы, упомянутые в других ответах, например
const userModel = mongoose.model<User & mongoose.Document>('User', schema);
источник
Просто добавьте еще один способ (
@types/mongoose
необходимо установить с помощьюnpm install --save-dev @types/mongoose
)import { IUser } from './user.ts'; import * as mongoose from 'mongoose'; interface IUserModel extends IUser, mongoose.Document {} const User = mongoose.model<IUserModel>('User', new mongoose.Schema({ userName: String, password: String, // ... }));
И разница между
interface
иtype
, прочтите этот ответУ этого способа есть преимущество, вы можете добавить типизацию статических методов Mongoose:
interface IUserModel extends IUser, mongoose.Document { generateJwt: () => string }
источник
generateJwt
?const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));
сути,generateJwt
становится еще одним свойством модели.IUser
объявления интерфейса в другом файле заключается в том, что риск рассинхронизации полей по мере роста сложности проекта и количества разработчиков довольно высок.Вот как это делают ребята из Microsoft. Вот
import mongoose from "mongoose"; export type UserDocument = mongoose.Document & { email: string; password: string; passwordResetToken: string; passwordResetExpires: Date; ... }; const userSchema = new mongoose.Schema({ email: { type: String, unique: true }, password: String, passwordResetToken: String, passwordResetExpires: Date, ... }, { timestamps: true }); export const User = mongoose.model<UserDocument>("User", userSchema);
Я рекомендую проверить этот отличный стартовый проект, когда вы добавите TypeScript в свой проект Node.
https://github.com/microsoft/TypeScript-Node-Starter
источник
ts-mongoose
иtypegoose
решают эту проблему, хотя, по общему признанию, содержат довольно много синтаксической неразберихи.С этим vscode intellisenseработает как на
Код:
// imports import { ObjectID } from 'mongodb' import { Document, model, Schema, SchemaDefinition } from 'mongoose' import { authSchema, IAuthSchema } from './userAuth' // the model export interface IUser { _id: ObjectID, // !WARNING: No default value in Schema auth: IAuthSchema } // IUser will act like it is a Schema, it is more common to use this // For example you can use this type at passport.serialize export type IUserSchema = IUser & SchemaDefinition // IUser will act like it is a Document export type IUserDocument = IUser & Document export const userSchema = new Schema<IUserSchema>({ auth: { required: true, type: authSchema, } }) export default model<IUserDocument>('user', userSchema)
источник
Вот пример из документации Mongoose, Создание из классов ES6 с использованием loadClass () , преобразованных в TypeScript:
import { Document, Schema, Model, model } from 'mongoose'; import * as assert from 'assert'; const schema = new Schema<IPerson>({ firstName: String, lastName: String }); export interface IPerson extends Document { firstName: string; lastName: string; fullName: string; } class PersonClass extends Model { firstName!: string; lastName!: string; // `fullName` becomes a virtual get fullName() { return `${this.firstName} ${this.lastName}`; } set fullName(v) { const firstSpace = v.indexOf(' '); this.firstName = v.split(' ')[0]; this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1); } // `getFullName()` becomes a document method getFullName() { return `${this.firstName} ${this.lastName}`; } // `findByFullName()` becomes a static static findByFullName(name: string) { const firstSpace = name.indexOf(' '); const firstName = name.split(' ')[0]; const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1); return this.findOne({ firstName, lastName }); } } schema.loadClass(PersonClass); const Person = model<IPerson>('Person', schema); (async () => { let doc = await Person.create({ firstName: 'Jon', lastName: 'Snow' }); assert.equal(doc.fullName, 'Jon Snow'); doc.fullName = 'Jon Stark'; assert.equal(doc.firstName, 'Jon'); assert.equal(doc.lastName, 'Stark'); doc = (<any>Person).findByFullName('Jon Snow'); assert.equal(doc.fullName, 'Jon Snow'); })();
Для статического
findByFullName
метода я не мог понять, как получить информацию о типеPerson
, поэтому мне пришлось выполнить приведение,<any>Person
когда я хочу его вызвать. Если вы знаете, как это исправить, добавьте комментарий.источник
ts-mongoose
илиtypegoose
. Ситуация еще больше дублируется при определении схемы GraphQL.Я поклонник Plumier, у него есть помощник мангуста , но его можно использовать автономно без самого Plumier . В отличие от Typegoose, он пошел по другому пути, используя специальную библиотеку отражений Plumier, которая позволяет использовать охлаждающие материалы.
Характеристики
T & Document
таким образом, ее можно получить доступ к свойствам, связанным с документом.strict:true
конфигурация tsconfig. И со свойствами параметра не требует декоратора для всех свойств.Применение
import model, {collection} from "@plumier/mongoose" @collection({ timestamps: true, toJson: { virtuals: true } }) class Domain { constructor( public createdAt?: Date, public updatedAt?: Date, @collection.property({ default: false }) public deleted?: boolean ) { } } @collection() class User extends Domain { constructor( @collection.property({ unique: true }) public email: string, public password: string, public firstName: string, public lastName: string, public dateOfBirth: string, public gender: string ) { super() } } // create mongoose model (can be called multiple time) const UserModel = model(User) const user = await UserModel.findById()
источник
Для тех, кто ищет решение для существующих проектов Mongoose:
Недавно мы создали mongoose-tsgen для решения этой проблемы (хотелось бы получить отзывы!). Существующие решения, такие как typegoose, требовали переписывания всех наших схем и внесения различных несовместимостей. mongoose-tsgen - это простой инструмент CLI, который генерирует файл index.d.ts, содержащий интерфейсы Typescript для всех ваших схем Mongoose; он практически не требует настройки и очень легко интегрируется с любым проектом TypeScript.
источник
Вот пример, основанный на README для
@types/mongoose
пакета.Помимо элементов, уже включенных выше, он показывает, как включать обычные и статические методы:
import { Document, model, Model, Schema } from "mongoose"; interface IUserDocument extends Document { name: string; method1: () => string; } interface IUserModel extends Model<IUserDocument> { static1: () => string; } var UserSchema = new Schema<IUserDocument & IUserModel>({ name: String }); UserSchema.methods.method1 = function() { return this.name; }; UserSchema.statics.static1 = function() { return ""; }; var UserModel: IUserModel = model<IUserDocument, IUserModel>( "User", UserSchema ); UserModel.static1(); // static methods are available var user = new UserModel({ name: "Success" }); user.method1();
В общем, этот README кажется фантастическим ресурсом для изучения типов с мангустами.
источник
IUserDocument
вUserSchema
, что создает риск обслуживания по мере усложнения модели. Пакеты вродеts-mongoose
иtypegoose
пытаются решить эту проблему, хотя, по общему признанию, имеют довольно много синтаксических ошибок.Если вы хотите убедиться, что ваша схема удовлетворяет типу модели и наоборот, это решение предлагает лучший набор текста, чем то, что предлагал @bingles:
Файл общего типа:
ToSchema.ts
(Не паникуйте! Просто скопируйте и вставьте его)import { Document, Schema, SchemaType, SchemaTypeOpts } from 'mongoose'; type NonOptionalKeys<T> = { [k in keyof T]-?: undefined extends T[k] ? never : k }[keyof T]; type OptionalKeys<T> = Exclude<keyof T, NonOptionalKeys<T>>; type NoDocument<T> = Exclude<T, keyof Document>; type ForceNotRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required?: false }; type ForceRequired = Omit<SchemaTypeOpts<any>, 'required'> & { required: SchemaTypeOpts<any>['required'] }; export type ToSchema<T> = Record<NoDocument<NonOptionalKeys<T>>, ForceRequired | Schema | SchemaType> & Record<NoDocument<OptionalKeys<T>>, ForceNotRequired | Schema | SchemaType>;
и пример модели:
import { Document, model, Schema } from 'mongoose'; import { ToSchema } from './ToSchema'; export interface IUser extends Document { name?: string; surname?: string; email: string; birthDate?: Date; lastLogin?: Date; } const userSchemaDefinition: ToSchema<IUser> = { surname: String, lastLogin: Date, role: String, // Error, 'role' does not exist name: { type: String, required: true, unique: true }, // Error, name is optional! remove 'required' email: String, // Error, property 'required' is missing // email: {type: String, required: true}, // correct 👍 // Error, 'birthDate' is not defined }; const userSchema = new Schema(userSchemaDefinition); export const User = model<IUser>('User', userSchema);
источник