Automattic / mongoose

MongoDB object modeling designed to work in an asynchronous environment.
https://mongoosejs.com
MIT License
26.88k stars 3.83k forks source link

InferSchemaType produces DocumentArray instead of a simple Array #13772

Open juona opened 1 year ago

juona commented 1 year ago

Prerequisites

Mongoose version

7.4.4

Node.js version

16.19.1

MongoDB server version

5

Typescript version (if applicable)

4.8.3

Description

The InferSchemaType utility converts subdocument arrays into DocumentArrays. My understanding is that this utility should produce a plain type that has nothing to do with Mongoose, so it should infer simple arrays instead of DocumentArrays. Is my expectation incorrect?

Possibly related: https://github.com/Automattic/mongoose/issues/12030 .

Steps to Reproduce

const mySchema = new Schema({
    nodes: {
        type: [
            new Schema({
                foo: String
            })
        ],
        required: true
    }
});

type IMySchema = InferSchemaType<typeof mySchema>;

Type IMySchema is:

type IMySchema = {
    nodes: Types.DocumentArray<{
        foo?: string | undefined;
    }>;
}

Expected Behavior

I'd expect type IMySchema to be:

type IMySchema = {
    nodes: Array<{
        foo?: string | undefined;
    }>;
}
vkarpov15 commented 1 year ago

"My understanding is that this utility should produce a plain type that has nothing to do with Mongoose": Ideally yes, the Types.DocumentArray<> workaround is one exception to that rule. We're trying to change that for 8.0.

vkarpov15 commented 1 year ago

As expected, going to be a bit hard to change this without #13856. Because InferSchemaType<> can't quite decide whether it wants to return the raw document type or the hydrated document type. And we don't want to do type transformations in HydratedDocument<> because of issues like #13523: mapping types in TypeScript get screwed up by generics, private fields, etc.

benmeiri commented 1 year ago

any workaround for this one?

vkarpov15 commented 12 months ago

Not currently, we're working on a workaround.

vkarpov15 commented 12 months ago

Going to move this out of 8.0 to a future release with the work we're doing on #13900. We haven't been able to make progress on this issue, and I don't want this to block 8.0 any further. Plus the work from #13900 is something we can release in a minor release.

FantinRaimbault commented 9 months ago

My solution :

const likeSchema = new Schema(
    {
        userId: { type: String, required: true },
        date: { type: Date, required: true },
    },
    {
        timestamps: true,
        _id: true,
    }
);

const postSchema = new Schema(
    {
        title: { type: String, required: true },
        content: { type: String, required: true },
        likes: { type: [likeSchema], required: true, default: [] },
    },
    {
        timestamps: true,
        _id: true,
    }
);

const userSchema = new Schema(
    {
        _id: { type: mongoose.Schema.Types.ObjectId, required: true },
        name: { type: String, required: true },
        lastName: { type: String, required: true },
        settings: {
            type: {
                isPublic: { type: Boolean, required: true, default: false },
                isForSale: { type: Boolean, required: true, default: false },
            },
            required: true,
            default: {},
        },
        age: { type: Number, required: false },
        posts: {
            type: [postSchema],
            required: true,
            default: [],
        },
    },
    {
        timestamps: true,
        _id: true,
    }
);

 type ReplaceMongooseDocumentArrayByArray<MySchema extends Record<string, any>> = {
    [K in keyof MySchema]: MySchema[K] extends mongoose.Types.DocumentArray<infer ArrayType>
        ? ReplaceMongooseDocumentArrayByArray<Omit<ArrayType, keyof mongoose.Types.Subdocument>>[]
        : MySchema[K] extends number | string | boolean | Date | mongoose.Schema.Types.ObjectId | mongoose.Types.ObjectId
        ? MySchema[K]
        : ReplaceMongooseDocumentArrayByArray<MySchema[K]>;
};

type User = ReplaceMongooseDocumentArrayByArray<InferSchemaType<typeof userSchema>>;
domharrington commented 1 month ago

Also came across this issue. I'm trying to use InferSchemaType to generate TS types for models that contain nested subdocuments. I have a suspicion it's only working for the top level when it should be recursing down into subdocuments if they exist.

Consider the following code:

import * as mongoose from 'mongoose'

const childSchema = new mongoose.Schema({ name: String });

const parentSchema = new mongoose.Schema({
  // Array of subdocuments
  children: [childSchema],
  // Single nested subdocuments
  child: childSchema
});

type ParentSchema = mongoose.InferSchemaType<typeof parentSchema>;

const Parent = mongoose.model('Parent', parentSchema);

function create(parent: ParentSchema) {
  return Parent.create(parent)
}

create({ child: { name: 'child' }, children: [{ name: 'another-child' }] })

This has a TS error when creating the children[]:

image

The type that comes out of InferSchemaType looks like this:

type ParentSchema = {
    children: mongoose.Types.DocumentArray<{
        name?: string | null | undefined;
    }>;
    child?: {
        name?: string | null | undefined;
    } | null | undefined;
}

The generated type should look like this:

type ParentSchema = {
    children: {
        name?: string | null | undefined;
    }[];
    child?: {
        name?: string | null | undefined;
    } | null | undefined;
};

You can work around this issue by doing the following:

type ParentSchema = Omit<mongoose.InferSchemaType<typeof parentSchema>, 'children'> & { children: mongoose.InferSchemaType<typeof childSchema>[] };

I can take a stab at fixing if you point me to where in the code I might look for this. Thanks in advance!

vkarpov15 commented 1 week ago

@domharrington I don't think you should use InferSchemaType for this purpose. Use InferRawDocType instead to get the schema's raw doc type as follows:

import * as mongoose from 'mongoose'

const childSchema = new mongoose.Schema({ name: String });

const parentSchemaDef = {
  // Array of subdocuments
  children: [childSchema],
  // Single nested subdocuments
  child: childSchema
};
const parentSchema = new mongoose.Schema(parentSchemaDef);

type ParentSchema = mongoose.InferRawDocType<typeof parentSchemaDef>;

const Parent = mongoose.model('Parent', parentSchema);

function create(parent: ParentSchema) {
  return Parent.create(parent)
}

create({ child: { name: 'child' }, children: [{ name: 'another-child' }] })

InferrRawDocType is more correct here because you want the POJO type from the schema definition, InferSchemaType is more to support Mongoose's internal automatic type inference.

domharrington commented 1 week ago

Oh interesting! I did not know about that one. That seems to do the trick, thank you!

domharrington commented 1 week ago

I tried refactoring my code to use InferRawDocType instead of InferSchemaType but now I'm coming up against another error (i think it's the same as this one to do with Dates: https://github.com/Automattic/mongoose/issues/14839 which it seems like has just had a fix merged in over here).

I've subscribed to that issue and will wait until that version gets released before trying again. The error I'm getting is this:

The types of 'toJSON(...).completedAt' are incompatible between these types.
  Type 'Date | null | undefined' is not assignable to type 'FlattenMaps<Date> | null | undefined'.
    Type 'Date' is missing the following properties from type 'FlattenMaps<Date>': expires, max, min, defaultOptions, and 21 more.ts(2345)

Which I think is the same!

vkarpov15 commented 1 week ago

Yeah that looks like the same issue as #14839. We will ship a fix for that this week.

domharrington commented 1 week ago

Yeah that looks like the same issue as #14839. We will ship a fix for that this week.

Thanks in advance! Appreciate your work on this as always.

vkarpov15 commented 5 days ago

@domharrington your issue should be fixed in v8.6.3