Intentando implementar un modelo de Mongoose en Typescript. El rastreo de Google ha revelado solo un enfoque híbrido (que combina JS y TS). ¿Cómo se implementaría la clase User, en mi enfoque bastante ingenuo, sin el JS?
Quiere poder IUserModel sin el equipaje.
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
Tim McNamara
fuente
fuente
User
no puede ser una clase porque crear una es una operación asincrónica. Tiene que devolver una promesa, así que tienes que llamarUser.create({...}).then...
.User
no puede ser una clase?Respuestas:
Así es como lo hago:
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;
fuente
import * as mongoose from 'mongoose';
oimport mongoose = require('mongoose');
import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
let newUser = new User({ iAmNotHere: true })
sin errores en el IDE o en la compilación. Entonces, ¿cuál es la razón para crear una interfaz?Otra alternativa si desea separar sus definiciones de tipo y la implementación de la base de datos.
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 */ }));
Inspiración desde aquí: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models
fuente
mongoose.Schema
definición aquí duplica los campos deIUser
? Dado queIUser
está definido en un archivo diferente, el riesgo de que los campos se desincronicen a medida que el proyecto crece en complejidad y en número de desarrolladores, es bastante alto.Perdón por la necropublicación, pero esto puede ser interesante para alguien. Creo que Typegoose proporciona una forma más moderna y elegante de definir modelos.
Aquí hay un ejemplo de los documentos:
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); })();
Para un escenario de conexión existente, puede usar lo siguiente (que puede ser más probable en situaciones reales y descubierto en los documentos):
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); })();
fuente
typegoose
no tenga suficiente soporte ... verificando sus estadísticas de npm, son solo 3k descargas semanales y hay casi 100 problemas abiertos de Github, la mayoría de los cuales no tienen comentarios, y algunos de los cuales parece que deberían haberse cerrado hace mucho tiempotypegoose
: terminamos manejando manualmente nuestra escritura, similar a esta publicación , parece quets-mongoose
podría tener alguna promesa (como se sugiere en la respuesta posterior)Prueba
ts-mongoose
. Utiliza tipos condicionales para hacer el mapeo.import { createSchema, Type, typedModel } from 'ts-mongoose'; const UserSchema = createSchema({ username: Type.string(), email: Type.string(), }); const User = typedModel('User', UserSchema);
fuente
La mayoría de las respuestas aquí repiten los campos en la clase / interfaz de TypeScript y en el esquema de mangosta. No tener una sola fuente de verdad representa un riesgo de mantenimiento, ya que el proyecto se vuelve más complejo y más desarrolladores trabajan en él: es más probable que los campos se desincronicen. . Esto es particularmente malo cuando la clase está en un archivo diferente al esquema de mangosta.
Para mantener los campos sincronizados, tiene sentido definirlos una vez. Hay algunas bibliotecas que hacen esto:
Ninguno de ellos me ha convencido todavía por completo, pero parece que typegoose se mantiene activamente y el desarrollador aceptó mis PR.
Para pensar un paso adelante: cuando agrega un esquema GraphQL a la mezcla, aparece otra capa de duplicación del modelo. Una forma de superar este problema podría ser generar código TypeScript y mongoose a partir del esquema GraphQL.
fuente
Aquí hay una forma fuerte de escribir un modelo sencillo con un esquema de mangosta. El compilador se asegurará de que las definiciones pasadas a mongoose.Schema coincidan con la interfaz. Una vez que tenga el esquema, puede usar
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 );
Una vez que tenga su esquema, puede usar los métodos mencionados en otras respuestas, como
const userModel = mongoose.model<User & mongoose.Document>('User', schema);
fuente
Simplemente agregue otra forma (
@types/mongoose
debe instalarse connpm 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, // ... }));
Y la diferencia entre
interface
ytype
, lea esta respuestaDe esta manera tiene una ventaja, puede agregar tipos de métodos estáticos de Mongoose:
interface IUserModel extends IUser, mongoose.Document { generateJwt: () => string }
fuente
generateJwt
?const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));
básicamente, segenerateJwt
convierte en otra propiedad del modelo.IUser
declaración de interfaz en un archivo diferente es que el riesgo de que los campos se desincronicen a medida que el proyecto aumenta en complejidad y desarrolladores, es bastante alto.Así es como lo hacen los chicos de Microsoft. aquí
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);
Recomiendo revisar este excelente proyecto de inicio cuando agregue TypeScript a su proyecto Node.
https://github.com/microsoft/TypeScript-Node-Starter
fuente
ts-mongoose
ytypegoose
resuelven ese problema, aunque es cierto que con un poco de cruft sintáctico.Con esto vscode intellisensefunciona en ambos
El código:
// 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)
fuente
Aquí está el ejemplo de la documentación de Mongoose, Creación de clases de ES6 usando loadClass () , convertido a 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'); })();
Para el
findByFullName
método estático , no pude imaginar cómo obtener la información de tipoPerson
, así que tuve que lanzar<any>Person
cuando quería llamarlo. Si sabe cómo solucionarlo, agregue un comentario.fuente
ts-mongoose
otypegoose
. La situación se duplica aún más al definir el esquema GraphQL.Soy fanático de Plumier, tiene un ayudante de mangosta , pero se puede usar de forma independiente sin Plumier. . A diferencia de Typegoose, tomó un camino diferente al usar la biblioteca de reflexión dedicada de Plumier, que hace posible usar cosas frescas.
Caracteristicas
T & Document
lo que es posible acceder a las propiedades relacionadas con el documento.strict:true
configuración tsconfig. Y con propiedades de parámetro no requiere decorador en todas las propiedades.Uso
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()
fuente
Para cualquiera que busque una solución para proyectos existentes de Mongoose:
Recientemente creamos mongoose-tsgen para abordar este problema (¡me encantaría recibir comentarios!). Las soluciones existentes como typegoose requirieron reescribir todos nuestros esquemas e introdujeron varias incompatibilidades. mongoose-tsgen es una herramienta CLI simple que genera un archivo index.d.ts que contiene interfaces de Typecript para todos sus esquemas de Mongoose; requiere poca o ninguna configuración y se integra muy bien con cualquier proyecto de TypeScript.
fuente
A continuación, se muestra un ejemplo basado en el archivo README del
@types/mongoose
paquete.Además de los elementos ya incluidos arriba, muestra cómo incluir métodos regulares y estáticos:
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();
En general, este archivo README parece ser un recurso fantástico para abordar tipos con mangostas.
fuente
IUserDocument
haciaUserSchema
, lo que crea un riesgo de mantenimiento a medida que el modelo se vuelve más complejo. A los paquetes les gustats-mongoose
etypegoose
intentan resolver ese problema, aunque es cierto que con un poco de cruft sintáctico.Si desea asegurarse de que su esquema satisfaga el tipo de modelo y viceversa, esta solución ofrece una mejor escritura de la que sugirió @bingles:
El archivo de tipo común:
ToSchema.ts
(¡Que no cunda el pánico! Simplemente cópielo y péguelo)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>;
y un modelo de ejemplo:
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);
fuente