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

DocumentArray constructor accepts array of any type what can cause runtime error #13087

Closed
2 tasks done
Jokero opened this issue Feb 26, 2023 · 9 comments · Fixed by #13089 or #13183
Closed
2 tasks done

DocumentArray constructor accepts array of any type what can cause runtime error #13087

Jokero opened this issue Feb 26, 2023 · 9 comments · Fixed by #13089 or #13183
Labels
typescript Types or Types-test related issue / Pull Request
Milestone

Comments

@Jokero
Copy link
Contributor

Jokero commented Feb 26, 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

6.10.0

Node.js version

18.12.0

MongoDB server version

N/A

Typescript version (if applicable)

4.9.5

Description

DocumentArray constructor accepts values: any[]:

class DocumentArray<T> extends Types.Array<T extends Types.Subdocument ? T : Types.Subdocument<InferId<T>> & T> {
    /** DocumentArray constructor */
    constructor(values: any[]);

This can result in runtime error when passed to constructor value won't have required field.

Steps to Reproduce

This code compiles and results in runtime error:

import { Types } from 'mongoose';

interface Book {
    author: {
        name: string;
    };
}

const books: Types.DocumentArray<Book> = new Types.DocumentArray<Book>([1, 2, 3]);
console.log(books[0].author.name);

Error:

console.log(books[0].author.name);
                            ^
TypeError: Cannot read properties of undefined (reading 'name')

Expected Behavior

Compile time error:

Type 'number' is not assignable to type 'Book'
@vkarpov15
Copy link
Collaborator

You're right, we should change any[] to AnyObject[] here.

@vkarpov15 vkarpov15 added this to the 6.10.1 milestone Feb 26, 2023
@vkarpov15 vkarpov15 added the typescript Types or Types-test related issue / Pull Request label Feb 26, 2023
@Jokero
Copy link
Contributor Author

Jokero commented Feb 26, 2023

@vkarpov15 Shouldn't it be T[]? AnyObject still allows any object, what can result in an error

@vkarpov15
Copy link
Collaborator

No, because of type casting. For example, [{ _id: '0'.repeat(24) }] is valid even if _id is expected to be of type ObjectId, because Mongoose will convert it.

@Jokero
Copy link
Contributor Author

Jokero commented Feb 26, 2023

My use case is that I have an interface with required fields, and currently with DocumentArray it's possible to forget to add one of these fields what can result in an error:

import { InferSchemaType, Schema, Types } from 'mongoose';

const locationSchema = new Schema(
    {
        type: {
            required: true,
            type: String,
            enum: ['Point'],
        },
        coordinates: {
            required: true,
            type: [Number], // [longitude, latitude]
        },
    },
    { _id: false }
);

const pointSchema = new Schema({
    name: { required: true, type: String },
    location: { required: true, type: locationSchema },
});

const routeSchema = new Schema({
    points: { type: [pointSchema] },
});

type Route = InferSchemaType<typeof routeSchema>;

function getTestRouteData(): Route {
    return {
        points: new Types.DocumentArray([
            { name: 'Test' } // "location" is missing
        ]),
    };
}

const { points } = getTestRouteData();
points.forEach(({ location }) => {
    console.log(`Point coordinates: ${location.coordinates[0]},${location.coordinates[1]}`);
});

// TypeError: Cannot read properties of undefined (reading 'coordinates')

@vkarpov15
Copy link
Collaborator

We'll work on a better workaround for this, but for now you can add default: () => ({}) to the location property as follows.

const pointSchema = new Schema({
    name: { required: true, type: String },
    location: { required: true, type: locationSchema, default: () => ({}) }, // <-- `default` here
});

That gives you the TypeError that you expect:

$ ./node_modules/.bin/tsc gh-13087.ts 
gh-13087.ts:31:9 - error TS2322: Type 'DocumentArray<{ name: string; }>' is not assignable to type 'DocumentArray<{ name: string; location: { type: "Point"; coordinates: number[]; }; }>'.
  Property 'location' is missing in type '{ name: string; }' but required in type '{ name: string; location: { type: "Point"; coordinates: number[]; }; }'.

31         points: new Types.DocumentArray([

@vkarpov15 vkarpov15 modified the milestones: 6.10.1, TypeScript backlog Feb 27, 2023
@Jokero
Copy link
Contributor Author

Jokero commented Feb 27, 2023

Maybe I need to change something else additionally, because it works same way as before 😄

@vkarpov15
Copy link
Collaborator

Weird. Well, at least if you add default: () => ({}), you won't get the same failure at runtime.

vkarpov15 added a commit that referenced this issue Mar 13, 2023
types(documentarray): typed DocumentArray constructor parameter
@Jokero
Copy link
Contributor Author

Jokero commented Mar 15, 2023

@vkarpov15 @lpizzinidev With 7.0.2 the code above (with added location property) can't be compiled at all:

error TS2322: Type 'DocumentArray<{ name: unknown; location: unknown; }>' is not assignable to type 'DocumentArray<{ name: string; location: { type: "Point"; coordinates: number[]; }; }>'.
  Type '{ name: unknown; location: unknown; }' is not assignable to type '{ name: string; location: { type: "Point"; coordinates: number[]; }; }'.
    Types of property 'name' are incompatible.
      Type 'unknown' is not assignable to type 'string'.

31         points: new Types.DocumentArray([
           ~~~~~~

  node_modules/mongoose/types/inferschematype.d.ts:28:74
    28    IsItRecordAndNotAny<EnforcedDocType> extends true ? EnforcedDocType : {
                                                                                ~
    29      [K in keyof (RequiredPaths<DocDefinition, TSchemaOptions['typeKey']> &
       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    30      OptionalPaths<DocDefinition, TSchemaOptions['typeKey']>)]: ObtainDocumentPathType<DocDefinition[K], TSchemaOptions['typeKey']>;
       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    31    };
       ~~~~
    The expected type comes from property 'points' which is declared here on type '{ points: DocumentArray<{ name: string; location: { type: "Point"; coordinates: number[]; }; }>; }'

Code:

import { InferSchemaType, Schema, Types } from 'mongoose';

const locationSchema = new Schema(
    {
        type: {
            required: true,
            type: String,
            enum: ['Point'],
        },
        coordinates: {
            required: true,
            type: [Number], // [longitude, latitude]
        },
    },
    { _id: false }
);

const pointSchema = new Schema({
    name: { required: true, type: String },
    location: { required: true, type: locationSchema },
});

const routeSchema = new Schema({
    points: { type: [pointSchema] },
});

type Route = InferSchemaType<typeof routeSchema>;

function getTestRouteData(): Route {
    return {
        points: new Types.DocumentArray([
            { name: 'Test', location: { type: 'Point', coordinates: [1, 2] } }
        ]),
    };
}

const { points } = getTestRouteData();
points.forEach(({ location }) => {
    console.log(`Point coordinates: ${location.coordinates[0]},${location.coordinates[1]}`);
});

@lpizzinidev
Copy link
Contributor

@vkarpov15 I guess that AnyObject[] is the way to go here.

vkarpov15 added a commit that referenced this issue Mar 17, 2023
types(documentarray): fixed type of DocumentArray constructor parameter
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
3 participants