猫鼬打字稿方式...?


90

试图在Typescript中实现Mongoose模型。对Google的调查只揭示了一种混合方法(结合JS和TS)。如果没有JS,如何以我比较幼稚的方式实现User类呢?

希望能够不带行李的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() {}
}

User不能是一个类,因为创建一个是异步操作。它必须返回承诺,所以您必须致电User.create({...}).then...
Louay Alakkad 2015年

1
具体来说,在OP中的代码中,您能否详细说明为什么User不能成为类?
蒂姆·麦克纳马拉2015年


@Erich他们说typeorm在MongoDB上不能很好地工作,也许Type goose是一个不错的选择
PayamBeirami

Answers:


130

这是我的方法:

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
抱歉,在TS中如何定义“猫鼬”?
蒂姆·麦克纳马拉2015年

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

1
像这样的东西:import User from '~/models/user'; User.find(/*...*/).then(/*...*/);
Louay Alakkad '16

3
最后一行(导出默认const User ...)不适用于我。我需要按照stackoverflow.com/questions/35821614/…的
Sergio

7
我可以let newUser = new User({ iAmNotHere: true })在IDE或编译时做到没有任何错误。那么创建接口的原因是什么?
Lupurus

33

如果要分离类型定义和数据库实现,则可以使用另一种方法。

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


1
mongoose.Schema这里的定义是否重复来自的字段IUser?鉴于IUser另一个文件中定义了该字段,因此随着项目的复杂性和开发人员数量的增加,字段不同步风险非常高。
Dan Dascalescu

是的,这是一个值得考虑的有效论点。使用组件集成测试可能有助于降低风险。并请注意,有一些方法和体系结构将类型声明和数据库实现分开,无论是通过ORM(如您所建议的)还是手动(如此答案)完成的。无银子弹有... <(°°)>
伽柏伊姆雷

一种方法可能是根据GraphQL定义为TypeScript和猫鼬生成代码
Dan Dascalescu

23

很抱歉为您发布尸体,但对于某些人来说仍然很有趣。我认为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);
})();

8
我也得出了这个结论,但担心typegoose没有足够的支持...检查他们的npm统计信息,每周仅下载3k,并且有近100个未解决的Github问题,其中大多数没有评论,并且其中一些看起来应该早就关闭了
Corbfon

@Corbfon您尝试过吗?如果是这样,您的发现是什么?如果没有,还有其他什么让您决定不使用它的?我通常会看到一些人担心是否会获得完整的支持,但是似乎真正使用它的人对此感到非常满意
N4ppeL

1
@ N4ppeL我不会接受typegoose-我们最终手动处理了我们的打字,类似于这篇文章,它看起来ts-mongoose可能有一些希望(如后面的回答中所建议)
Corbfon

1
永远不要为“坏帖”道歉。[您现在知道...]甚至还有一个徽章(尽管命名为死灵法师; ^ D)!鼓励发布新信息和新想法!
鲁芬

1
@ruffin:我也真的不理解针对发布新的和最新的问题解决方案的污名。
Dan Dascalescu

16

尝试ts-mongoose。它使用条件类型进行映射。

import { createSchema, Type, typedModel } from 'ts-mongoose';

const UserSchema = createSchema({
  username: Type.string(),
  email: Type.string(),
});

const User = typedModel('User', UserSchema);

1
看起来很有前途!感谢分享!:)
Boriel

1
哇。这锁非常圆滑。期待尝试一下!
qqilihq

1
披露:ts-猫鼬似乎是由天空创造的。似乎是最精巧的解决方案。
麦克风

1
不错的包装,您还在维护吗?
Dan Dascalescu

11

这里的大多数答案都是在TypeScript类/接口和猫鼬模式中重复这些字段。由于项目变得越来越复杂,并且有更多的开发人员在此工作,没有单一的事实来源会带来维护风险:字段更可能不同步。当类与猫鼬模式位于不同的文件中时,这尤其糟糕。

为了使字段保持同步,定义一次即可。有一些库可以做到这一点:

我还没有完全相信它们,但是typegoose似乎得到了积极维护,并且开发人员接受了我的PR。

想想下一步:将GraphQL模式添加到混合中时,会出现另一层模型复制。解决此问题的一种方法可能是从GraphQL模式生成TypeScript和猫鼬代码


5

这是一种将普通模型与猫鼬模式匹配的强类型化方法。编译器将确保传递给mongoose.Schema的定义与接口匹配。一旦有了模式,就可以使用

共同点

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]>
};

用户名

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

1
这是唯一正确的答案。没有其他答案实际上可以确保架构与类型/接口之间的类型兼容性。
杰米·斯特劳斯

@JamieStrauss:首先不复制字段怎么办?
Dan Dascalescu

1
@DanDascalescu我不认为您了解类型如何工作。
Jamie Strauss

5

只需添加另一种方式(@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
rels

1
const User = mongoose.model.... password: String, generateJwt: () => { return someJwt; } }));基本上,@ relsgenerateJwt成为模型的另一个属性。
a11smiles'2

您将以这种方式将其添加为方法还是将其连接到methods属性?
user1790300'7

1
这应该是可接受的答案,因为它会分离用户定义和用户DAL。如果要从mongo切换到另一个数据库提供程序,则无需更改用户界面。
拉斐尔·德尔里约

1
@RafaeldelRio:问题是关于在TypeScript中使用猫鼬。切换到另一个数据库与该目标相反。将模式定义与不同文件中IUser接口声明分开的问题在于,随着项目的复杂性和开发人员数量的增加,字段不同步风险非常高。
Dan Dascalescu

4

微软的人就是这样。这里

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


1
这会在猫鼬和TypeScript之间复制每个字段,这会在模型变得更加复杂时带来维护风险。解决方案喜欢ts-mongoosetypegoose解决了这个问题,尽管公认有很多语法缺陷。
Dan Dascalescu

2

与此同时vscode intellisense工作

  • 用户类型 User.findOne
  • 用户实例 u1._id

编码:

// 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

这是Mongoose文档中的示例,使用loadClass()从ES6类创建,并转换为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-mongoosetypegoose。定义GraphQL模式时,情况会进一步重复。
Dan Dascalescu

用这种方法可以定义引用吗?
Dan Dascalescu

1

我是Plumier的粉丝,它有猫鼬帮手但没有Plumier本身也可以单独使用。与Typegoose不同,它通过使用Plumier的专用反射库采用了不同的路径,从而可以使用炫酷的东西。

特征

  1. 纯POJO(域无需从任何类继承,也不需要使用任何特殊的数据类型),自动创建的模型可以由此推断出T & Document可以访问与文档相关的属性。
  2. 支持的TypeScript参数属性,当您使用strict:truetsconfig配置时,这很好。使用参数属性并不需要在所有属性上使用装饰器。
  3. 受支持的字段属性,例如Typegoose
  4. 配置与猫鼬相同,因此您将很容易熟悉它。
  5. 支持的继承使编程更加自然。
  6. 模型分析,显示模型名称及其适当的集合名称,应用的配置等。

用法

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

对于正在寻找现有Mongoose项目解决方案的任何人:

我们最近构建了mongoose-tsgen来解决此问题(希望获得一些反馈!)。现有的解决方案(例如typegoose)需要重写我们的整个架构,并引入各种不兼容性。mongoose-tsgen是一个简单的CLI工具,它会生成一个index.d.ts文件,其中包含所有Mongoose模式的Typescript接口;它几乎不需要任何配置,并且可以与任何Typescript项目非常平滑地集成。


0

这是一个基于自述文件@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();

通常,此自述文件似乎是使用猫鼬处理类型的绝佳资源。


这种方法从IUserDocument到复制了每个字段的定义,UserSchema随着模型变得更加复杂,这会带来维护风险。程序包喜欢ts-mongoosetypegoose尝试解决该问题,尽管公认有很多语法缺陷。
Dan Dascalescu

0

如果要确保您的架构满足模型类型,反之亦然,则此解决方案比@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);


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.