-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typescript type inference from mongoose Schema #9715
Comments
Also, the prototype I referenced above can probably be generally improved; any and all suggestions welcome. I'll create a small repo which showcases this feature in a minimal mongoose-typescript setup when I can. |
I've been using ts-mongoose for this. It's somewhat limited since the flow is necessarily: |
We have a few ideas about how to support this, but have not implemented this yet. We will likely add this in the future. |
Ok, Great! I'd love to help implement this if I can. |
Nothing concrete enough to provide a code sample yet. But I'm very much open to suggestions @nitzanhen |
Ok, so I've done a bit of research. Incorporating
|
Implementing EntityAs for implementing the Entity type, I believe we can simply improve on the Below is a list of all the types & features of Mongoose schemas that
Note: We need to consider maybe implementing multiple, similar variants of Have I missed something? |
It would be great if this feature would support export const BlaSchema: Schema = new Schema({
displayName: {
type: String,
}
});
export class Bla {
speak(): void {
console.log(this.displayName); // <-- Currently this will fail to transpile because it doesn't know "displayName" should be part of the class "Bla"
}
}
BotSchema.loadClass(Bla); |
@BorntraegerMarc To be honest, I didn't know about Schema's I spot two problematic points in implementing type safety for The latter, as far as I know, has no "proper" solution in Typescript - you would have to explicitly assert, somewhere, that As for the former, I think it should be pretty easily doable using Typescript's declaration merging feature - the class remains as it is, and beside it an interface with the same name (e.g. //Note: the `Schema` type annotation in your example needs to be removed for this to work!
export const BlaSchema = new Schema({
displayName: {
type: String,
}
});
export interface Bla extends mongoose.infer<typeof BlaSchema> {}; //New line; merges with the class
export class Bla {
speak(): void {
console.log(this.displayName); // Now typescript should know about this
}
}
//For convenience
export interface BlaDocument extends Bla, Document {}
BotSchema.loadClass(Bla);
//...
//Later, when declaring the model, we need to explicitly declare the new methods from `Bla`.
/** @todo: test this; do the static and instance methods of Bla need to be separated? */
const BlaModel = mongoose.model<BlaDocument, Model<BlaDocument & Bla /*??*/>>("Bla", BlaSchema) My main assumption here is the the inferred type, e.g. I haven't played around with |
@nitzanhen I see your point: I also wouldn't know of a way for the problem nr. 2: "getting Typescript to acknowledge the loaded methods". So about the general If so, I'm happy to open a new issue with some documentation suggestions. Does anyone disagree with @nitzanhen's approach that it is the best / cleanest one? On a different note: When copy/pasting your code typescript complains Furthermore: according to my tests export const BlaSchema = new Schema({
displayName: {
type: String,
},
});
export interface Bla {
displayName: string;
}
export class Bla {
speak(): void {
console.log(this.displayName);
}
}
export interface BlaDocument extends Bla, Document {}
BlaSchema.loadClass(Bla);
const BlaModel = mongoose.model<BlaDocument>('Bla', BlaSchema);
BlaModel.create({ displayName: '' }).then((b) => {
b.speak();
}); EDIT: I'm an idiot 😄 Just noticed now that |
One more thing that came to my mind when dealing with type inference from mongoose Schema: import { Document, model, Schema, SchemaTypes } from 'mongoose';
export const BlaSchema = new Schema({
displayName: {
type: SchemaTypes.String,
},
language: {
type: SchemaTypes.String,
required: [true],
default: 'english',
},
});
export interface Bla {
displayName: string;
language: string;
}
export interface BlaDocument extends Bla, Document {}
const BlaModel = model<BlaDocument>('Bla', BlaSchema);
BlaModel.create({ displayName: '' }); // <--- This line throws TS error "No overload matches this call. Property 'language' is missing." |
I had the same issue and managed to overcome in the following way :
Its not perfect as you still have the original problem that this issue describes eg duplication of mongoose and typescript definition. |
@UncleVic I'm not sure what you're referring to. This thread discussed a suggestion related to inferring typescript types from a Mongoose schema. If it's related, please clarify, otherwise - please open another issue and describe your problem thoroughly. |
I totally agree. As far as I know Mongoose doesn't have proper documentation for Typescript users yet, and having implemented official Typescript types, now would be a good time to start working on them in my opinion. That being said, it's a whole lot of work, and I'm guessing most maintainers of this library have higher priorities. Have you opened another card for it? Also, sorry for disappearing, I'm having a busy couple of months. I'll try to make some progress with this when I'm able to. And of course, more suggestions or contributions are always welcome. |
Here's how I solved this
Now there's no need to double define interfaces. |
To follow on from @animaonline Heres a Unfortunately it will still require you to maintain the SDL and the TS Interface, so a change to the underlying TS interface will need to be made in the Schema definition as well, which is a little annoying. But to work backwards and infer the type from the Schema Definition would require internal changes to mongoose I believe. Developed with mongoose 5.12.0 export function TypedSchema<ISchema>(
schema: SchemaDefinition<_AllowStringsForIds<LeanDocument<ISchema>>>
) {
return new Schema(schema);
} Usage export interface ISymbol {
symbol: string;
securityName: string;
isEtf: boolean;
exchange: string;
country: string;
quote: ObjectId;
}
export const Symbol = TypedSchema<ISymbol>({
symbol: String,
securityName: String,
isEtf: false,
exchange: String,
country: String,
quote: { type: Schema.Types.ObjectId, ref: `Quote` },
}); |
Seems this feature was implemented in #11563. Thanks for everyone involved! |
Do you want to request a feature or report a bug? feature
What is the current behavior?
Defining a model in a project with mongoose & typescript currently involves defining the schema twice - once as a mongoose schema, and once as a Typescript interface/type. This makes maintenance more difficult, especially for large models or models with nested fields/schemas.
What is the expected behavior?
I think it would be great (both in terms of convenience and of good coding) if there was an official way to infer the model as a Typescript type from the schema. I actually have created a prototype implementation in one of my projects:
However, there are two main things to be noted about this:
First of all, this prototype something I put a few hours into, and supports all schema features I had in the project I built this for. Therefore, it should support Mongoose's schemas' basic features, but probably not all of them. That being said, I think most of the information that would be declared in a Typescript interface (e.g. the type of a field or its existence; some information, such as validation, default values or uniqueness, cannot be declared in Typescript unfortunately) can be deduces from the schema using Typescript's tools.
Second, and more important, is that this was built from outside, looking at mongoose as a black box of sorts. If we implement something of the sort inside mongoose, there may be additional types exposed to us that can make this definition easier or clearer.
as for the API, I think something akin to
(this draws inspiration from Zod's api)
would be good, although it depends on the way this feature is eventually implemented, of course.
What are the versions of Node.js, Mongoose and MongoDB you are using? Note that "latest" is not a version.
Node.js v14.15.1, mongoose v5.11.8.
I'm using typescript 4.1.2; the implementation would need to use Typescript's
infer
keyword heavily, so the minimal version of Typescript that mongoose can be used with would need to support it.The text was updated successfully, but these errors were encountered: