Manipule o modo de escrita datilografada ...?


94

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() {}
}

Usernão pode ser uma classe porque criar uma é uma operação assíncrona. Tem que retornar uma promessa, então você tem que ligar User.create({...}).then....
Louay Alakkad

1
Especificamente, dado no código do OP, você poderia explicar por que Usernão pode ser uma classe?
Tim McNamara

Em vez disso, tente github.com/typeorm/typeorm .
Erich

@Erich, eles dizem que typeorm não funciona bem com o MongoDB, talvez Type goose seja uma boa opção
PayamBeirami

Respostas:


133

É 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;

2
desculpe, mas como 'mangusto' é definido no TS?
Tim McNamara

13
import * as mongoose from 'mongoose';ouimport mongoose = require('mongoose');
Louay Alakkad

1
Algo assim:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad

3
Última linha (exportar usuário const padrão ...) não funciona para mim. Preciso dividir a linha, conforme proposto em stackoverflow.com/questions/35821614/…
Sergio

7
Posso fazer let newUser = new User({ iAmNotHere: true })sem erros no IDE ou na compilação. Então, qual é a razão para criar uma interface?
Lupurus

34

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


1
A mongoose.Schemadefinição aqui duplica os campos de IUser? Dado que IUserestá 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.
Dan Dascalescu

Sim, este é um argumento válido que vale a pena considerar. O uso de testes de integração de componentes pode ajudar a reduzir os riscos. E observe que há abordagens e arquiteturas em que as declarações de tipo e as implementações de banco de dados são separadas, seja por meio de um ORM (como você propôs) ou manualmente (como nesta resposta). Não existe bala de prata ... <(°. °)>
Gábor Imre

Um marcador pode ser gerar código a partir da definição GraphQL, para TypeScript e mongoose.
Dan Dascalescu,

24

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);
})();

8
Eu também cheguei a esta conclusão, mas estou preocupado que typegoosenã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 tempo
Corbfon

@Corbfon Você experimentou? Se sim, quais foram suas descobertas? Se não, houve mais alguma coisa que fez você decidir não usá-lo? Geralmente vejo algumas pessoas se preocupando com o suporte completo, mas parece que aqueles que realmente o usam estão muito felizes com ele
N4ppeL

1
@ N4ppeL Eu não aceitaria typegoose- acabamos lidando com nossa digitação manualmente, semelhante a este post , parece que ts-mongoosepode ser promissor (como sugerido na resposta posterior)
Corbfon

1
Nunca se desculpe por "necroposting". [Como você já sabe ...] Há até um crachá (embora ele é chamado Necromancer ; ^ D) para fazer exatamente isso! Necroposting novas informações e idéias é incentivado!
Ruffin

1
@ruffin: Eu também não entendo o estigma contra postar soluções novas e atualizadas para problemas.
Dan Dascalescu,

16

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);

1
Parece muito promissor! Obrigado por compartilhar! :)
Boriel

1
Uau. Este bloqueia muito elegante. Estou ansioso para experimentar!
qqilihq

1
Divulgação: ts-mongoose parece ser criado pelo céu. Parece ser a solução mais inteligente que existe.
microfone de


11

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.


5

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);

1
Esta é a única resposta correta. Nenhuma das outras respostas realmente garantiu a compatibilidade de tipo entre o esquema e o tipo / interface.
Jamie Strauss


1
@DanDascalescu Acho que você não entende como os tipos funcionam.
Jamie Strauss,

5

Basta adicionar outra forma ( @types/mongoosedeve ser instalado com 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,
    // ...
}));

E a diferença entre interfacee type, leia esta resposta

Desta forma, tem uma vantagem, você pode adicionar tipificações de método estático Mongoose:

interface IUserModel extends IUser, mongoose.Document {
  generateJwt: () => string
}

onde você definiu generateJwt?
rels

1
@rels const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));basicamente, generateJwttorna-se outra propriedade do modelo.
a11smiles de

Você o adicionaria como um método desta maneira ou o conectaria à propriedade de métodos?
user1790300

1
Esta deve ser a resposta aceita, pois separa a definição do usuário e a DAL do usuário. Se você quiser mudar de mongo para outro provedor de banco de dados, não precisará alterar a interface do usuário.
Rafael del Rio

1
@RafaeldelRio: a questão era sobre como usar o mangusto com TypeScript. Mudar para outro banco de dados é contrário a esse objetivo. E o problema de separar a definição do esquema da IUserdeclaraçã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.
Dan Dascalescu,

4

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


1
Isso duplica todos os campos entre o mangusto e o TypeScript, o que cria um risco de manutenção conforme o modelo se torna mais complexo. As soluções gostam ts-mongoosee typegooseresolvem esse problema, embora reconhecidamente com um pouco de dificuldade sintática.
Dan Dascalescu,

2

Com isso vscode intellisensefunciona em ambos

  • Tipo de usuário User.findOne
  • instância de usuário u1._id

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)


2

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 findByFullNamemétodo estático , não consegui descobrir como obter as informações de tipo Person, então tive que lançar <any>Personquando quiser chamá-lo. Se você sabe como consertar isso, por favor, adicione um comentário.


Como outras respostas , essa abordagem duplica os campos entre a interface e o esquema. Isso poderia ser evitado tendo uma única fonte de verdade, por exemplo, usando ts-mongooseou typegoose. A situação fica ainda mais duplicada ao definir o esquema GraphQL.
Dan Dascalescu

Alguma maneira de definir refs com essa abordagem?
Dan Dascalescu

2

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

  1. Puro POJO (o domínio não precisa herdar de nenhuma classe, nem usando nenhum tipo de dado especial), o Modelo criado automaticamente infere para T & Documentque seja possível acessar as propriedades relacionadas ao documento.
  2. Propriedades de parâmetro TypeScript com suporte, é bom quando você tem strict:true configuração tsconfig. E com propriedades de parâmetro não requer decorador em todas as propriedades.
  3. Propriedades de campo com suporte, como Typegoose
  4. A configuração é igual à do mangusto, portanto você se familiarizará facilmente com ela.
  5. Herança com suporte que torna a programação mais natural.
  6. Análise de modelo, mostrando nomes de modelo e seu nome de coleção apropriado, configuração aplicada etc.

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()

1

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.


1

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);



0

Aqui está um exemplo baseado no README do @types/mongoosepacote.

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.


Essa abordagem duplica a definição de cada campo de IUserDocumentem UserSchema, o que cria um risco de manutenção conforme o modelo se torna mais complexo. Os pacotes gostam ts-mongoosee typegoosetentam resolver esse problema, embora reconhecidamente com um pouco de dificuldade sintática.
Dan Dascalescu

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.