Extended Joi not implementing custom operators - javascript

I have issues extending joi class with custom operators. I want to validate mongodb Ids, but the extended object throws following error:
error: uncaughtException: JoiObj.string(...).objectId is not a function
TypeError: JoiObj.string(...).objectId is not a function
Code is following:
import Joi from 'joi';
import * as mongodb from 'mongodb';
interface ExtendedStringSchema extends Joi.StringSchema {
objectId(): this;
}
interface ExtendedJoi extends Joi.Root {
string(): ExtendedStringSchema;
}
const JoiObj: ExtendedJoi = Joi.extend({
base: Joi.string(),
type: 'objectId',
messages: {
'objectId.invalid': '"{{#label}}" must be a valid mongo id'
},
validate(value, helpers) {
if (!mongodb.ObjectId.isValid(value)) {
return helpers.error('objectId.invalid');
}
return value;
}
});
const objIdSchema = JoiObj.object({
id: JoiObj.string().objectId()
});
I found 2 examples:
https://github.com/sideway/joi/issues/2357
How to extend a module from npm using TypeScript?
however they use different properties than what is described in TS definition file and thus does not work.

You want to extend the Joi.string() base. Keep in mind, that you can't validate new mongodb.ObjectID() because it is of type object. You extended Joi.string() and this checks first if your value is of type string. And it will stop validating if it isn't. You can only validate new mongodb.ObjectID().toHexString() which looks like: "5f91a1449b13e3010c5548a2".
This answers is using joi 17.2.1 and mongodb 3.6.2
import Joi from 'joi';
import * as mongodb from 'mongodb';
interface ExtendedStringSchema extends Joi.StringSchema {
objectId(): this;
}
interface ExtendedJoi extends Joi.Root {
string(): ExtendedStringSchema;
}
const stringObjectExtension: Joi.Extension = {
type: 'string',
base: Joi.string(),
messages: {
'string.objectId': '{{#label}} must be a valid mongo id'
},
rules: {
objectId: {
validate: (value: any, helpers) => {
if (!mongodb.ObjectId.isValid(value)) {
return helpers.error('string.objectId')
}
return value;
}
}
}
};
// create extended Joi
const JoiObj: ExtendedJoi = Joi.extend(stringObjectExtension);
// create new mongodb id
const id = new mongodb.ObjectID();
const objIdSchema = JoiObj.object({
id: JoiObj.string().objectId()
});
// will fail because it won't pass the Joi.string() validation
const data1 = {
id: id
};
console.log(objIdSchema.validate(data1).error);
// will succeed
const data2 = {
id: id.toHexString()
};
console.log(objIdSchema.validate(data2).error);

I also had this problem. I solved it with this package.
https://www.npmjs.com/package/joi-oid
const Joi = require('joi-oid')
const schema = Joi.object({
id: Joi.objectId(),
name: Joi.string(),
age: Joi.number().min(18),
})
Good luck :)

Related

ajv - Ensure that given object structure matches structure of a custom type

I'm using ajv with TypeScript and have a custom type MyCustomType. When creating a validation schema I want to ensure that a specific property is of type MyCustomType. So ajv should validate its structure and decide whether this could be parsed to the given type.
I started with the following sample code ( and created a Codesandbox example for testing purposes )
import { AType } from "./AType"; // custom type
import { AnInterface } from "./AnInterface"; // custom interface
import Ajv from "ajv";
type MyComplexType = AType | AnInterface; // create a more complex type from the given imports
const ajvInstance = new Ajv();
const schema = {
type: "object",
properties: {
myComplexType: { type: "string" } // value structure must match structure of MyComplexType
},
required: ["myComplexType"],
additionalProperties: false
};
const validate = ajvInstance.compile(schema);
const data = {
myComplexType: {}
};
const isValid = validate(data);
if (isValid) {
console.info("everything is fine");
} else {
validate.errors?.forEach((error) => console.error(error.message));
}
Currently I don't know how to create a validation schema for the property myComplexType. I found some discussions
Add instanceof property for type objects
Creating a custom type
Type keywords
but I don't think these will help because
typeof just returns "object"
instanceof won't work for types
So do I have to create a custom keyword ( as described here ) and write my own validation logic ( inspect the object structure ) or are there any things I can already use? How should I configure myComplexType?
I flattened your structure to be able to answer with one code segment. This is TypeScript, and runs on NODE JS successfully using Ajv.
The key is using the JSONSchemaType from Ajv to bride between Ajv and TypeScript. If the TYPE and AJV definitions dont match up, TypeScript will complain. This is great, especially if your editor has a TypeScript real-time checker built in (VS Code, VIM, ...)
Notice that the schema MyComplexTypeSchema is defined using the previously defined schemas. It does not have to be one huge nested schema. Have a schema for every TYPE and INTERFACE, and build your library from the ground up to support both TypeScript and Ajv.
'use strict'
import Ajv, {JSONSchemaType} from "ajv"
const ajvInstance = new Ajv();
type AType = "x" | "y";
const ATypeSchema : JSONSchemaType<AType> = {
type:"string",
enum:["x","y"]
}
interface AnInterface {
prop: string;
}
const AnInterfaceSchema : JSONSchemaType<AnInterface> = {
type:"object",
properties:{
prop:{type:"string"}
},
required:["prop"]
}
type MyComplexType = AType | AnInterface;
const MyComplexTypeSchema : JSONSchemaType<MyComplexType> = {
oneOf:[ ATypeSchema,AnInterfaceSchema ]
}
const schema = {
type: "object",
properties: {
myComplexType: MyComplexTypeSchema
},
required: ["myComplexType"],
additionalProperties: false
};
const validate = ajvInstance.compile(schema);
const data1 = {
myComplexType: {
prop:"a string"
}
};
var isValid = validate(data1);
if (isValid) {
console.info("everything is fine in data1");
} else {
validate.errors?.forEach((error) => console.error(error.message));
}
const data2 = {
myComplexType: "x"
};
var isValid = validate(data2);
if (isValid) {
console.info("everything is fine in data2");
} else {
validate.errors?.forEach((error) => console.error(error.message));
}
const bad1 = {
myComplexType: {}
};
var isValid = validate(bad1);
if (isValid) {
console.info("everything is fine in bad1");
} else {
validate.errors?.forEach((error) => console.error("bad1: "+error.message));
}
const bad2 = {
myComplexType: "z"
};
var isValid = validate(bad2);
if (isValid) {
console.info("everything is fine in bad2");
} else {
validate.errors?.forEach((error) => console.error("bad2: "+error.message));
}
I left your "const schema" code alone, but it probably should be something like below, to be consistent with the other Type / Schema structuring.
type TopType = {
myComplexType : MyComplexType
}
const TopTypeSchema : JSONSchemaType<TopType> = {
type: "object",
properties: {
myComplexType: MyComplexTypeSchema
},
required: ["myComplexType"],
additionalProperties: false
};
const validate2 = ajvInstance.compile(TopTypeSchema);
// TypeScript will not let us build a BAD value if the type is defined
const data1:TopType = {
myComplexType: {
prop:"a string"
}
};
I don't think you can create a custom type with this library, maybe you're confusing javascript types with typescript types. So you probably have 2 options: 1) instead of using types you use classes and use instanceof for validating an instance of your class, e.g.
class MyClass {}
const instanceofDef = require("ajv-keywords/dist/definitions/instanceof")
instanceofDef.CONSTRUCTORS.MyClass = MyClass
ajv.validate({instanceof: "MyClass"}, new MyClass())
or 2) you create a nested schema like this
const schema = {
type: "object",
properties: {
myComplexType: {
type: "object",
properties: {
myProperty: { type: "string" },
myFunction: { type: "function" },
...
}
}
},
required: ["myComplexType"],
additionalProperties: false
};
const validate = ajvInstance.compile(schema);
const data = {
myComplexType: {
myProperty: "Hello World",
myFunction: function(){}
}
};
const isValid = validate(data);

Typescript and Mongoose: Property 'x' does not exist on type 'Document'

This is my Mongoose model that I use together with TypeScript:
import mongoose, { Schema } from "mongoose";
const userSchema: Schema = new Schema(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
},
name: {
type: String,
maxlength: 50,
},
...
...
}
);
userSchema.method({
transform() {
const transformed = {};
const fields = ["id", "name", "email", "createdAt", "role"];
fields.forEach((field) => {
transformed[field] = this[field];
});
return transformed;
},
});
userSchema.statics = {
roles,
checkDuplicateEmailError(err: any) {
if (err.code === 11000) {
var error = new Error("Email already taken");
return error;
}
return err;
},
};
export default mongoose.model("User", userSchema);
I use this model in my controller:
import { Request, Response, NextFunction } from "express";
import User from "../models/user.model";
import httpStatus from "http-status";
export const register = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const user = new User(req.body);
const savedUser = await user.save();
res.status(httpStatus.CREATED);
res.send(savedUser.transform());
} catch (error) {
return next(User.checkDuplicateEmailError(error));
}
};
I get the following errors:
Property 'transform' does not exist on type 'Document'.
Property 'checkDuplicateEmailError' does not exist on type 'Model<Document, {}>'.
I tried export default mongoose.model<any>("User", userSchema); and I do not get the transform error but still the error for checkDuplicateEmailError.
You know that mongoose.model("User", userSchema); creates a Model, but the question is: a model of what?
Without any type annotations, the model User gets the type Model<Document, {}> and the user object created from new User() gets the type Document. So of course you are going to get errors like "Property 'transform' does not exist on type 'Document'."
When you added your <any> variable, the type for user became any. Which actually gives us less information than knowing that user is a Document.
What we want to do is create a model for specific type of Document describing our user. Instances of the user should have a method transform() while the model itself should have the method checkDuplicateEmailError(). We do this by passing generics to the mongoose.model() function:
export default mongoose.model<UserDocument, UserModel>("User", userSchema);
The hard part is figuring out those two types. Frustratingly, mongoose doesn't automatically apply the fields from your schema as properties of the type, though there are packages that do this. So we have to write them out as typescript types.
interface UserDocument extends Document {
id: number;
name: string;
email: string;
createdAt: number;
role: string;
transform(): Transformed;
}
Our transform function returns a object with 5 specific properties from the UserDocument. In order to access the names of those properties without having to type them again, I moved the fields from inside your transform method to be a top-level property. I used as const to keep their types as string literals rather than just string. (typeof transformFields)[number] gives us the union of those strings.
const transformFields = ["id", "name", "email", "createdAt", "role"] as const;
type Transformed = Pick<UserDocument, (typeof transformFields)[number]>
Our UserModel is a Model of UserDocument and it also includes our checkDuplicateEmailError function.
interface UserModel extends Model<UserDocument> {
checkDuplicateEmailError(err: any): any;
}
We should also add the UserDocument generic when we create our Schema, so that this will have the type UserDocument when we access it inside a schema method.
const userSchema = new Schema<UserDocument>({
I got all sorts of typescript errors trying to implement the transform() method including missing index signatures. We can avoid reinventing the wheel here by using the pick method from lodash. I still had problems with the mongoose methods() helper function, but it works fine using the direct assignment approach.
userSchema.methods.transform = function (): Transformed {
return pick(this, transformFields);
};
You could also use destructuring to avoid the index signature issues.
userSchema.methods.transform = function (): Transformed {
const {id, name, email, createdAt, role} = this;
return {id, name, email, createdAt, role};
}
Within your email check function, I added a typeof check to avoid runtime errors from trying to access the property err.code if err is undefined.
if ( typeof err === "object" && err.code === 11000) {
That should fix all your errors.
Playground Link

How to define and use a related Model in Objection.js

Using the following code (which uses ES6's "type":"module" in package.json), I can't seem to access the related Model Group:
import db from "../connection.js";
import objection from "objection";
const { Model } = objection;
Model.knex(db);
class User extends Model {
static get tableName() {
return "users";
}
static get relationMappings() {
return {
groups: {
relation: Model.ManyToManyRelation,
modelClass: Group,
join: {
from: "users.id",
through: {
from: "users_groups.user_id",
to: "users_groups.group_id",
},
to: "groups.id",
}
}
}
}
}
class Group extends Model {
static get tableName() {
return "groups";
}
}
If I run
const myUser = await User.query().findById(1)
It outputs:
User {id: 1, name: "r", email: "raj#raj.raj", username: "raj", … }
But I still can't access the Group relation:
myUser.groups
Outputs:
undefined
What am I doing wrong?
You have to use eager loading in the query to load the desired relations.
It you are using Objection.js v1:
const myUser = await User.query().eager('groups').findById(1)
And since Objection.js v2, eager was renamed as withGraphFetched:
const myUser = await User.query().withGraphFetched('groups').findById(1)
Extra: Loading relations after instantiation
You can load the relations after instantiation using $relatedQuery. Note all instance methods starts with $:
const myUser = await User.query().findById(1)
const groupsOfMyUser = await myUser.$relatedQuery('groups')

How to define custom query helper in mongoose model with typescript?

I want to define custom query helper using query helper api .
Here the example:
// models/article.ts
import { Document, Schema, Model, model } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
ArticleSchema.query.byName = function(name) {
return this.find({ name })
}
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
// routes/article.ts
import ArticleModel from '../models/article.ts'
router.get('/articles, (req, res) => {
ArticleModel.find().byName('example')
})
Typescript complains about byName method when I chain it with defaults.
I can put it in IArticleModel interface but in that case I could only call it from model.
Where should I put the definition of this method to use it in chainable way?
I've drafted a new version of #types/mongoose that supports query helpers. See this answer for ways to install a modified #types package. With my version, you should be able to write the following in models/article.ts:
import { Document, Schema, Model, model, DocumentQuery } from 'mongoose';
interface IArticle extends Document {
name: string;
}
interface IArticleModel extends Model<IArticle, typeof articleQueryHelpers> {
someStaticMethod(): Promise<any>;
}
const ArticleSchema = new Schema( { name: String } )
let articleQueryHelpers = {
byName(this: DocumentQuery<any, IArticle>, name: string) {
return this.find({ name });
}
};
ArticleSchema.query = articleQueryHelpers;
export default model<IArticle, IArticleModel>('Article', ArticleSchema);
and then routes/article.ts will work. If this works for you, then I will submit a pull request to the original package on DefinitelyTyped.

Mongoose the Typescript way...?

Trying to implement a Mongoose model in Typescript. Scouring the Google has revealed only a hybrid approach (combining JS and TS). How would one go about implementing the User class, on my rather naive approach, without the JS?
Want to be able to IUserModel without the baggage.
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() {}
}
Here's how I do it:
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;
Another alternative if you want to detach your type definitions and the database implementation.
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 */
}));
Inspiration from here: https://github.com/Appsilon/styleguide/wiki/mongoose-typescript-models
Most answers here repeat the fields in the TypeScript class/interface, and in the mongoose schema. Not having a single source of truth represents a maintenance risk, as the project becomes more complex and more developers work on it: fields are more likely to get out of sync. This is particularly bad when the class is in a different file vs. the mongoose schema.
To keep fields in sync, it makes sense to define them once. There are a few libraries that do this:
typeodm.io - full test coverage, good examples, no traction yet
mongoose-decorators-ts - best English, no traction yet
typegoose - most popular, documentation needs improvement
ts-mongoose - doesn't use decorators, second most popular, not actively maintained
mongoose-schema-decorators - no traction yet
mongoose-typescript - fork of typegoose
I haven't yet been fully convinced by any of them but typegoose seems actively maintained, and the developer accepted my PRs.
To think one step ahead: when you add a GraphQL schema into the mix, another layer of model duplication appears. One way to overcome this problem might be to generate TypeScript and mongoose code from the GraphQL schema.
Sorry for necroposting but this can be still interesting for someone.
I think Typegoose provides more modern and elegant way to define models
Here is an example from the docs:
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);
})();
For an existing connection scenario, you can use as the following (which may be more likely in the real situations and uncovered in the docs):
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);
})();
Try ts-mongoose. It uses conditional types to do the mapping.
import { createSchema, Type, typedModel } from 'ts-mongoose';
const UserSchema = createSchema({
username: Type.string(),
email: Type.string(),
});
const User = typedModel('User', UserSchema);
Here's a strong typed way to match a plain model with a mongoose schema. The compiler will ensure the definitions passed to mongoose.Schema matches the interface. Once you have the schema, you can use
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
);
Once you have your schema, you can use methods mentioned in other answers such as
const userModel = mongoose.model<User & mongoose.Document>('User', schema);
Just add another way (#types/mongoose must be installed with 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,
// ...
}));
And the difference between interface and type, please read this answer
This way has a advantage, you can add Mongoose static method typings:
interface IUserModel extends IUser, mongoose.Document {
generateJwt: () => string
}
Here's how guys at Microsoft do it. here
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);
I recommend to check this excellent starter project out when you add TypeScript to your Node project.
https://github.com/microsoft/TypeScript-Node-Starter
If you want to ensure that your schema satisfies the model type and vice versa , this solution offers better typing than what #bingles suggested:
The common type file:
ToSchema.ts (Don't panic! Just copy and paste it)
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>;
and an example 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);
I am a fans of Plumier, it has mongoose helper, but it can be used standalone without Plumier itself. Unlike Typegoose its took different path by using Plumier's dedicated reflection library, that make it possible to use cools stuff.
Features
Pure POJO (domain doesn't need to inherit from any class, nor using any special data type), Model created automatically inferred as T & Document thus its possible to access document related properties.
Supported TypeScript parameter properties, it's good when you have strict:true tsconfig configuration. And with parameter properties doesn't require decorator on all properties.
Supported field properties like Typegoose
Configuration is the same as mongoose so you will get easily familiar with it.
Supported inheritance that's make the programming more natural.
Model analysis, showing model names and its appropriate collection name, configuration applied etc.
Usage
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()
2023 Update
The new recommended way of typing documents is using a single interface. To type documents in your application, you should use HydratedDocument:
import { HydratedDocument, model, Schema } from "mongoose";
interface Animal {
name: string;
}
const animalSchema = new Schema<Animal>({
name: { type: String, required: true },
});
const AnimalModel = model<Animal>("Animal", animalSchema);
const animal: HydratedDocument<Animal> = AnimalModel.findOne( // ...
Mongoose advises against extending document.
https://mongoosejs.com/docs/typescript.html
Here is the example from Mongoose documentation, Creating from ES6 Classes Using loadClass(), converted to 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');
})();
For the static findByFullName method, I couldn't figure how get the type information Person, so I had to cast <any>Person when I want to call it. If you know how to fix that please add a comment.
With this vscode intellisense works on both
User Type User.findOne
user instance u1._id
The Code:
// 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)
I find the following approach the easiest and most efficient since it validates the keys in the schema with the extra interface you define, helping you keep everything in sync.
You also get the amazing typescript autocomplete suggestions when you are adding/changing schema validator properties like maxlength, lowercase, etc on the schema.
Win win!
import { Document, model, Schema, SchemaDefinitionProperty } from "mongoose";
type TDocument<Fields> = Fields & Document;
type TSchema<Fields> = Record<keyof Fields, SchemaDefinitionProperty>;
type UserFields = {
email: string;
firstName?: string;
roles?: string[];
};
const userSchema: TSchema<UserFields> = {
email: { type: Schema.Types.String, required: true, index: true },
firstName: { type: Schema.Types.String, maxlength: 30, trim: true },
roles: [
{ type: Schema.Types.String, maxlength: 20, lowercase: true },
],
};
export const User = model<TDocument<UserFields>>(
"User",
new Schema(userSchema, { timestamps: true })
);
Best part! you could reuse TDocument and TSchema types for all your models.
For anyone looking for a solution for existing Mongoose projects:
We recently built mongoose-tsgen to address this issue (would love some feedback!). Existing solutions like typegoose required rewriting our entire schemas and introduced various incompatibilities. mongoose-tsgen is a simple CLI tool which generates an index.d.ts file containing Typescript interfaces for all your Mongoose schemas; it requires little to no configuration and integrates very smoothly with any Typescript project.
Official documents discourage TS interface to extend Document.
This approach works, but we recommend your document interface not extend Document. Using extends Document makes it difficult for Mongoose to infer which properties are present on query filters, lean documents, and other cases.
TS Interface
export interface IPerson {
firstName: string;
lastName: string;
fullName: string;
}
Schema
const personSchema = new Schema<IPerson>({
//You get intellisense of properties so less error prone
firstName:{type:String},
lastName:{type:String}
})
personSchema.virtual('fullName').get(function(this:IPerson) {
return this.firstName + " " this.lastName
});
export const User = model<IPerson>('person',personSchema)
Mongoose introduced officially supported TypeScript bindings in v5.11.0.
https://mongoosejs.com/docs/typescript.html describes Mongoose's recommended approach to working with Mongoose in TypeScript.
Here is an example based off the README for the #types/mongoose package.
Besides the elements already included above it shows how to include regular and static methods:
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();
In general, this README appears to be a fantastic resource for approaching types with mongoose.
The latest mongoose package has come with typescript support. You don't need to use #types/mongoose anymore. See my example here.
https://jasonching2005.medium.com/complete-guide-for-using-typescript-in-mongoose-with-lean-function-e55adf1189dc
Well, I found the following link really really helpful where the author has described each and every step in details without using any library.
Typescript With MongoDB and Node/Express
This has really really helped me and hoping will be very helpful for those searching for a solution without installing any extra plugin.
However, if you like you can give a try to TypeORM and TypeGoose
But I prefer to go without installing any library :-).
v6.9 update
You don't need to create a type or an interface no more. You only need a schema to generate the corresponding types:
import { model, Schema, HydratedDocumentFromSchema, InferSchemaType } from "mongoose";
const UserSchema = new Schema({
name: { type: String, required: true },
somethingElse: Number
});
// Already typed
export const UserModel = model('User', UserSchema);
// Type of an hydrated document (with all the getters, etc...)
export type THydratedUserModel = HydratedDocumentFromSchema<typeof UserSchema>;
// Only the fields defined in the shema
export type TUserModel = InferSchemaType<typeof UserSchema>;
⚠️ As of writing this, these type helpers (HydratedDocumentFromSchema and InferSchemaType) are undocumented.
As per mongoose docs
Alternatively, your document interface can extend Mongoose's Document
class.
We strongly recommend against using this approach, its support will be
dropped in the next major version as it causes major performance
issues.
Instead you can use HydratedDocument
export interface User {
name: string;
email: string;
password: string;
phone: string;
address: string[];
orders: ObjectId[];
}
export type UserDoc = HydratedDocument<User>
Not sure this is what you are looking for
but there's a package called Typegoose
TypeORM is a better and modern solution. It supports both JavaScript and TypeScript.
TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8).
It has lots of features.
Its goal is to always support the latest JavaScript features and provide additional features that help you to develop any kind of application that uses databases - from small applications with a few tables to large scale enterprise applications with multiple databases.
It supports most databases like mysql, mariadb, postgres, cockroachdb, sqlite, mssql, oracle, etc. and mongodb as well.
TypeORM supports both Active Record and Data Mapper patterns, unlike
all other JavaScript ORMs currently in existence, which means you can
write high quality, loosely coupled, scalable, maintainable
applications the most productive way.
So no need to learn different ORM or frameworks for different databases.

Categories