Skip to content
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

InferSchemaType produces DocumentArray instead of a simple Array #13772

Open
2 tasks done
juona opened this issue Aug 23, 2023 · 13 comments
Open
2 tasks done

InferSchemaType produces DocumentArray instead of a simple Array #13772

juona opened this issue Aug 23, 2023 · 13 comments
Labels
typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@juona
Copy link

juona commented Aug 23, 2023

Prerequisites

  • I have written a descriptive issue title
  • I have searched existing issues to ensure the bug has not already been reported

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: #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
Copy link
Collaborator

"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
Copy link
Collaborator

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
Copy link

any workaround for this one?

@vkarpov15
Copy link
Collaborator

Not currently, we're working on a workaround.

@vkarpov15
Copy link
Collaborator

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.

@vkarpov15 vkarpov15 modified the milestones: 8.0, Parking Lot Sep 28, 2023
@FantinRaimbault
Copy link

FantinRaimbault commented Dec 14, 2023

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

vkarpov15 added a commit that referenced this issue May 15, 2024
@domharrington
Copy link

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 vkarpov15 modified the milestones: Parking Lot, 8.6.2 Sep 6, 2024
@vkarpov15
Copy link
Collaborator

@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.

@vkarpov15 vkarpov15 modified the milestones: 8.6.2, 8.8 Sep 11, 2024
@domharrington
Copy link

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

@domharrington
Copy link

domharrington commented Sep 12, 2024

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: #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
Copy link
Collaborator

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

@domharrington
Copy link

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
Copy link
Collaborator

@domharrington your issue should be fixed in v8.6.3

@vkarpov15 vkarpov15 modified the milestones: 8.8, 8.9 Oct 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typescript Types or Types-test related issue / Pull Request
Projects
None yet
Development

No branches or pull requests

5 participants