Próba zaimplementowania modelu Mongoose w Typescript. Przeszukiwanie Google ujawniło tylko podejście hybrydowe (łączące JS i TS). Jak można by zaimplementować klasę User, stosując moje raczej naiwne podejście, bez JS?
Chcesz mieć możliwość IUserModel bez bagażu.
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
źródło
źródło
User
nie może być klasą, ponieważ jej utworzenie jest operacją asynchroniczną. Musi zwrócić obietnicę, więc musisz zadzwonićUser.create({...}).then...
.User
nie może być klasą?Odpowiedzi:
Oto jak to robię:
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;
źródło
import * as mongoose from 'mongoose';
lubimport mongoose = require('mongoose');
import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
let newUser = new User({ iAmNotHere: true })
bez żadnych błędów w IDE lub przy kompilacji. Jaki jest więc powód tworzenia interfejsu?Inna alternatywa, jeśli chcesz odłączyć definicje typów i implementację bazy danych.
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 */ }));
Inspiracja stąd: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models
źródło
mongoose.Schema
definicja tutaj powiela pola zIUser
? Biorąc pod uwagę, żeIUser
jest zdefiniowana w innym pliku na ryzyko, że pola z niezsynchronizowane jak projekt rośnie w złożoności i liczby deweloperów, jest dość wysoki.Przepraszamy za nekroposty, ale to może być nadal interesujące dla kogoś. Myślę, że Typegoose zapewnia bardziej nowoczesny i elegancki sposób definiowania modeli
Oto przykład z dokumentów:
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); })();
W przypadku istniejącego scenariusza połączenia można użyć następujących (co może być bardziej prawdopodobne w rzeczywistych sytuacjach i ujawnione w dokumentacji):
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); })();
źródło
typegoose
nie ma wystarczającego wsparcia ... sprawdzam statystyki npm, to tylko 3k tygodniowych pobrań, a rn jest prawie 100 otwartych problemów na Githubie, z których większość nie ma komentarzy, a niektóre z nich wyglądają tak, jakby powinny były zostać zamknięte dawno temutypegoose
- skończyło się na ręcznej obsłudze naszego pisania, podobnie jak w tym poście , wygląda na to, żets-mongoose
może mieć jakąś obietnicę (jak zasugerowano w późniejszej odpowiedzi)Spróbuj
ts-mongoose
. Do mapowania używa typów warunkowych.import { createSchema, Type, typedModel } from 'ts-mongoose'; const UserSchema = createSchema({ username: Type.string(), email: Type.string(), }); const User = typedModel('User', UserSchema);
źródło
Większość odpowiedzi tutaj powtarza pola w klasie / interfejsie TypeScript oraz w schemacie mangusta. Brak jednego źródła prawdy stwarza ryzyko związane z konserwacją, ponieważ projekt staje się bardziej złożony i pracuje nad nim więcej programistów: istnieje większe prawdopodobieństwo, że pola stracą synchronizację . Jest to szczególnie złe, gdy klasa znajduje się w innym pliku niż schemat mangusty.
Aby zachować synchronizację pól, warto je raz zdefiniować. Jest kilka bibliotek, które to robią:
Nie byłem jeszcze w pełni przekonany do żadnego z nich, ale typegoose wydaje się aktywnie utrzymywany, a deweloper zaakceptował moje PR.
Aby pomyśleć o krok naprzód: po dodaniu schematu GraphQL do miksu pojawia się kolejna warstwa powielania modelu. Jednym ze sposobów rozwiązania tego problemu może być wygenerowanie kodu TypeScript i kodu mongoose ze schematu GraphQL.
źródło
Oto skuteczny sposób na dopasowanie prostego modelu do schematu mangusty. Kompilator upewni się, że definicje przekazane do mongoose.Schema są zgodne z interfejsem. Gdy masz już schemat, możesz użyć
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 );
Gdy masz już swój schemat, możesz użyć metod wymienionych w innych odpowiedziach, takich jak
const userModel = mongoose.model<User & mongoose.Document>('User', schema);
źródło
Po prostu dodaj inny sposób (
@types/mongoose
musi być zainstalowany znpm 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, // ... }));
A różnica między
interface
itype
, przeczytaj tę odpowiedźTen sposób ma tę zaletę, że możesz dodać statyczne typy metod Mongoose:
interface IUserModel extends IUser, mongoose.Document { generateJwt: () => string }
źródło
generateJwt
?const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));
Zasadniczo @relsgenerateJwt
staje się kolejną właściwością modelu.IUser
deklaracji interfejsu w innym pliku polega na tym, że ryzyko braku synchronizacji pól w miarę wzrostu złożoności projektu i liczby programistów jest dość wysokie.Oto, jak robią to ludzie z Microsoftu. tutaj
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);
Polecam sprawdzić ten doskonały projekt początkowy po dodaniu TypeScript do projektu Node.
https://github.com/microsoft/TypeScript-Node-Starter
źródło
ts-mongoose
itypegoose
rozwiązują ten problem, choć trzeba przyznać, że zawiera sporo syntaktycznego okrucieństwa.Dzięki temu vscode intellisensedziała na obu
Kod:
// 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)
źródło
Oto przykład z dokumentacji Mongoose, Tworzenie z klas ES6 przy użyciu loadClass () , przekonwertowany na 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'); })();
W przypadku
findByFullName
metody statycznej nie mogłem dowiedzieć się, jak uzyskać informacje o typiePerson
, więc musiałem rzutować,<any>Person
gdy chcę to wywołać. Jeśli wiesz, jak to naprawić, dodaj komentarz.źródło
ts-mongoose
lubtypegoose
. Sytuacja ulega dalszemu powieleniu podczas definiowania schematu GraphQL.Jestem fanem Plumiera, ma pomocnika mangusty , ale można go używać samodzielnie bez samego Plumiera . W przeciwieństwie do Typegoose, wybrał inną ścieżkę, używając dedykowanej biblioteki refleksji Plumier, która umożliwia korzystanie z fajnych rzeczy.
cechy
T & Document
umożliwia dostęp do właściwości związanych z dokumentem.strict:true
konfigurację tsconfig. A z parametrami właściwości nie wymaga dekoratora we wszystkich właściwościach.Stosowanie
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()
źródło
Dla każdego, kto szuka rozwiązania dla istniejących projektów Mongoose:
Niedawno stworzyliśmy mongoose-tsgen, aby rozwiązać ten problem (chcielibyśmy poznać opinie!). Istniejące rozwiązania, takie jak typegoose, wymagały przepisania całych naszych schematów i wprowadziły różne niezgodności. mongoose-tsgen to proste narzędzie CLI, które generuje plik index.d.ts zawierający interfejsy Typescript dla wszystkich schematów Mongoose; wymaga niewielkiej konfiguracji i bardzo płynnie integruje się z każdym projektem Typescript.
źródło
Oto przykład oparty na pliku README
@types/mongoose
pakietu.Oprócz elementów już zawartych powyżej pokazuje, jak uwzględnić metody regularne i statyczne:
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();
Ogólnie rzecz biorąc, ten plik README wydaje się być fantastycznym źródłem informacji o podejściu do gatunków z mangustą.
źródło
IUserDocument
doUserSchema
, co stwarza ryzyko konserwacji, gdy model staje się bardziej złożony. Pakiety lubiąts-mongoose
itypegoose
próbują rozwiązać ten problem, choć trzeba przyznać, że zawierają sporo syntaktycznego okrucieństwa.Jeśli chcesz mieć pewność, że Twój schemat jest zgodny z typem modelu i odwrotnie, to rozwiązanie oferuje lepsze wpisywanie niż sugerowane @bingles:
Typowy plik typu:
ToSchema.ts
(Nie panikuj! Po prostu skopiuj i wklej)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>;
i przykładowy model:
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);
źródło