Tentando implementar um modelo Mongoose em Typescript. Vasculhar o Google revelou apenas uma abordagem híbrida (combinando JS e TS). Como alguém faria para implementar a classe User, em minha abordagem um tanto ingênua, sem o JS?
Quer ser capaz de IUserModel sem a bagagem.
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
fonte
fonte
User
não pode ser uma classe porque criar uma é uma operação assíncrona. Tem que retornar uma promessa, então você tem que ligarUser.create({...}).then...
.User
não pode ser uma classe?Respostas:
É assim que eu faço:
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;
fonte
import * as mongoose from 'mongoose';
ouimport mongoose = require('mongoose');
import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
let newUser = new User({ iAmNotHere: true })
sem erros no IDE ou na compilação. Então, qual é a razão para criar uma interface?Outra alternativa se você deseja desanexar suas definições de tipo e a implementação do banco de dados.
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 */ }));
Inspiração daqui: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models
fonte
mongoose.Schema
definição aqui duplica os campos deIUser
? Dado queIUser
está definido em um arquivo diferente, o risco de os campos ficarem fora de sincronia conforme o projeto cresce em complexidade e número de desenvolvedores é bastante alto.Desculpe por necropostar, mas isso ainda pode ser interessante para alguém. Acho que Typegoose oferece uma maneira mais moderna e elegante de definir modelos
Aqui está um exemplo dos 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 um cenário de conexão existente, você pode usar o seguinte (o que pode ser mais provável em situações reais e descoberto nos 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); })();
fonte
typegoose
não tenha suporte suficiente ... verificando suas estatísticas de npm, são apenas 3 mil downloads semanais e há quase 100 problemas abertos no Github, a maioria dos quais sem comentários, e alguns dos quais parecem que deveriam ter sido fechados há muito tempotypegoose
- acabamos lidando com nossa digitação manualmente, semelhante a este post , parece quets-mongoose
pode ser promissor (como sugerido na resposta posterior)Experimente
ts-mongoose
. Ele usa tipos condicionais para fazer o mapeamento.import { createSchema, Type, typedModel } from 'ts-mongoose'; const UserSchema = createSchema({ username: Type.string(), email: Type.string(), }); const User = typedModel('User', UserSchema);
fonte
A maioria das respostas aqui repete os campos na classe / interface TypeScript e no esquema mongoose. Não ter uma única fonte de verdade representa um risco de manutenção, conforme o projeto se torna mais complexo e mais desenvolvedores trabalham nele: os campos têm maior probabilidade de ficar fora de sincronia . Isso é particularmente ruim quando a classe está em um arquivo diferente do esquema mongoose.
Para manter os campos sincronizados, faz sentido defini-los uma vez. Existem algumas bibliotecas que fazem isso:
Eu ainda não fui totalmente convencido por nenhum deles, mas typegoose parece ativamente mantido, e o desenvolvedor aceitou meus PRs.
Para pensar um passo à frente: quando você adiciona um esquema GraphQL à combinação, outra camada de duplicação do modelo aparece. Uma maneira de superar esse problema pode ser gerar código TypeScript e mongoose a partir do esquema GraphQL.
fonte
Esta é uma maneira forte de combinar um modelo simples com um esquema de mangusto. O compilador garantirá que as definições passadas para mongoose.Schema correspondam à interface. Depois de ter o esquema, você pode 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 );
Depois de ter seu esquema, você pode usar os métodos mencionados em outras respostas, como
const userModel = mongoose.model<User & mongoose.Document>('User', schema);
fonte
Basta adicionar outra forma (
@types/mongoose
deve ser instalado comnpm 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, // ... }));
E a diferença entre
interface
etype
, leia esta respostaDesta forma, tem uma vantagem, você pode adicionar tipificações de método estático Mongoose:
interface IUserModel extends IUser, mongoose.Document { generateJwt: () => string }
fonte
generateJwt
?const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));
basicamente,generateJwt
torna-se outra propriedade do modelo.IUser
declaração da interface em um arquivo diferente é que o risco de os campos ficarem fora de sincronia conforme o projeto aumenta em número de complexidade e desenvolvedores é bastante alto.Veja como os caras da Microsoft fazem isso. aqui
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);
Eu recomendo verificar este excelente projeto inicial ao adicionar TypeScript ao seu projeto Node.
https://github.com/microsoft/TypeScript-Node-Starter
fonte
ts-mongoose
etypegoose
resolvem esse problema, embora reconhecidamente com um pouco de dificuldade sintática.Com isso vscode intellisensefunciona em ambos
O 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)
fonte
Aqui está o exemplo da documentação do Mongoose, Criando a partir de classes ES6 usando loadClass () , convertido para 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 o
findByFullName
método estático , não consegui descobrir como obter as informações de tipoPerson
, então tive que lançar<any>Person
quando quiser chamá-lo. Se você sabe como consertar isso, por favor, adicione um comentário.fonte
ts-mongoose
outypegoose
. A situação fica ainda mais duplicada ao definir o esquema GraphQL.Eu sou um fã do Plumier, ele tem um ajudante de mangusto , mas pode ser usado sozinho, sem o próprio Plumier . Ao contrário do Typegoose, ele tomou um caminho diferente usando a biblioteca de reflexão dedicada de Plumier, que torna possível usar o cools.
Características
T & Document
que seja possível acessar as propriedades relacionadas ao documento.strict:true
configuração tsconfig. E com propriedades de parâmetro não requer decorador em todas as propriedades.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()
fonte
Para quem procura uma solução para projetos Mongoose existentes:
Recentemente, construímos o mongoose-tsgen para resolver esse problema ( adoraríamos receber algum feedback!). Soluções existentes, como typegoose, exigiam a reescrita de todos os nossos esquemas e introduziam várias incompatibilidades. mongoose-tsgen é uma ferramenta CLI simples que gera um arquivo index.d.ts contendo interfaces Typescript para todos os seus esquemas Mongoose; ele requer pouca ou nenhuma configuração e se integra perfeitamente a qualquer projeto Typescript.
fonte
Se você deseja garantir que seu esquema satisfaça o tipo de modelo e vice-versa, esta solução oferece uma digitação melhor do que a sugerida por @bingles:
O tipo de arquivo comum:
ToSchema.ts
(não entre em pânico! Basta copiar e colar)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>;
e um modelo de exemplo:
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);
fonte
Aqui está um exemplo baseado no README do
@types/mongoose
pacote.Além dos elementos já incluídos acima, mostra como incluir métodos regulares e 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();
Em geral, este README parece ser um recurso fantástico para abordar tipos com mangusto.
fonte
IUserDocument
emUserSchema
, o que cria um risco de manutenção conforme o modelo se torna mais complexo. Os pacotes gostamts-mongoose
etypegoose
tentam resolver esse problema, embora reconhecidamente com um pouco de dificuldade sintática.O pacote mongoose mais recente vem com suporte para digitação. Você não precisa mais usar @ types / mongoose. Veja meu exemplo aqui.
https://jasonching2005.medium.com/complete-guide-for-using-typescript-in-mongoose-with-lean-function-e55adf1189dc
fonte