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

Compile error when using the mongoose Boolean as a type in a sub-object in a schema. #14361

Closed pauIbanez closed 6 months ago

pauIbanez commented 6 months ago

Prerequisites

Mongoose version

7.3.1

Node.js version

v16.14.0

MongoDB server version

5.6.0

Typescript version (if applicable)

5.1.6

Description

When creating a typed mongoose schema, if a property in a sub-object is of type Boolean and the syntax is "extended":

interface Test {
information: {
  isBug: boolean;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    isBug: {
      type: Boolean,
      required: true,
    },
  },
})

instead of:

interface Test {
information: {
  isBug: boolean;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    isBug: Boolean,
  },
})

The compiler will fail with the following error:

PATH\node_modules\ts-node\src\index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:

further down:

Object literal may only specify known properties, and '_id' does not exist in type

What's weird about it is the following, normally the _id: false is allowed to be sharing a space with the object information properties as long as no other "declarations" are done. If I want to say define a default information I would have to use something like:

interface Test {
information: {
  isBug: boolean;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    default: {isBug: true},
    type: {
      isBug: {
        type: Boolean,
        required: true,
      },
    },
  },
})

The problem bug or at least very sus activity comes from this. ANY type other than Boolean is accepted directly, the only one that fails (That I know) is boolean. See the steps to reproduce for more info, as there I expand on this.

Steps to Reproduce

The issue arises when you have a structure like this:

interface Test {
information: {
  name: boolean;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: Boolean,
      required: true,
    },
  },
})

While if you have the same with, say, a string, it does work as expected and compiles normally:

interface Test {
information: {
  name: string;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: String,
      required: true,
    },
  },
})

If you have it in "conditional" syntax it will also compile normally:

interface Test {
information: {
  name: boolean; 
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: Boolean
  },
})

Any non-defined or non-equal type will also not cause an error, so something like:

interface Test {
information: {
  name: boolean; // <- Note this is a boolean in the type
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: String,
      required: true,
    },
  },
})

or:

interface Test {
information: {
  name: string; 
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: Boolean, // <- Note this is a boolean in the schema
      required: true,
    },
  },
})

Expected Behavior

I'd expect all the types to act the same and ether be accepted with the _id: false parameter in the object or not, but seems like Boolean is the only exception that I've found.

I want to also note a very easy workaround to this, which is to use the type property:

interface Test {
information: {
  name: boolean;
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    type: {
      name: {
        type: Boolean,
        required: true,
      },
    },
  },
})

But the fact that this can be solved this way does not excuse the issue.

I hope I explained myself

pauIbanez commented 6 months ago

I have to add here that to reproduce this error you need to load the a model with this schema and then actually load the model somewhere in a program, even if you don't call it in runtime.

From some more experimentation I've also encountered that all of these cases can fail as well, but I have no idea what makes them fail in some test and others they pass normally:

interface Test {
information: {
  name: boolean; // <- Note this is a boolean in the type
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: String,
      required: true,
    },
  },
})

or:

interface Test {
information: {
  name: string; 
  };
};

const schema = new Schema<Test>({
  information: {
    _id: false,
    name: {
      type: Boolean, // <- Note this is a boolean in the schema
      required: true,
    },
  },
})

as well as having a property defined in ether schema or interface that the other does not have

IslandRhythms commented 6 months ago
import * as mongoose from 'mongoose';

interface Test {
  information: {
    isBug: boolean;
  }
}

const testSchema = new mongoose.Schema<Test>({
  information: {
    _id: false,
    isBug: {
      type: Boolean,
      required: true
    }
  }
});

const TestModel = mongoose.model<Test>('Test', testSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await TestModel.create({ information: { isBug: true } });
}

run();
pauIbanez commented 6 months ago

After experimenting some more it seems that inferring the type in a schema overall does not work as expected. I've mainly had the Boolean issue, but it turns out that for some other types gives similar issues.

I've found that if you use the property enum in any of the fields it also breaks the compiler:

import * as mongoose from 'mongoose';

enum TestEnum{
  hello = "hi",
  bye = "good ridance"
}

interface Test {
  information: {
    isBug: TestEnum;
  }
}

const testSchema = new mongoose.Schema<Test>({
  information: {
    _id: false,
    isBug: {
      type: String,
      required: true,
      enum: TestEnum
    }
  }
});

const TestModel = mongoose.model<Test>('Test', testSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await TestModel.create({ information: { isBug: true } });
}

run();

In some other cases, the compiler fails in an even more unexpected way, but I've sadly not been able to reproduce. I had a this setup (as a workaround):

import * as mongoose from 'mongoose';

enum TestEnum{
  hello = "hi",
  bye = "good ridance"
}

interface Test {
  information: {
    isBug: TestEnum;
  }
}

const testSchema = new mongoose.Schema<Test>({
  information: {
    _id: false,
    type: {
      isBug: {
        type: String,
        required: true,
        enum: TestEnum
      }
    }
  }
});

const TestModel = mongoose.model<Test>('Test', testSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await TestModel.create({ information: { isBug: true } });
}

run();

And I got a similar issue basically telling me that "information" did not have a property called type.

My best guess as to why this happens is that when mongoose converts the types: { isBug?: SchemaDefinitionProperty<boolean>;} it does not add the property _id as to WHY this happens, the only real connection between the failing properties and the _id property that I could find is that they are both boolean, but as I just explained here enums fail aswell so I'm not so sure this is the case anymore.

vkarpov15 commented 6 months ago

Are you able to upgrade to TypeScript 5.2? I can confirm that the following scripts fail in TypeScript 5.1.6, but compile fine in TypeScript 5.2.

import * as mongoose from 'mongoose';

interface Test {
  information: {
    isBug: boolean;
  }
}

const testSchema = new mongoose.Schema<Test>({
  information: {
    _id: false,
    isBug: {
      type: Boolean,
      required: true
    }
  }
});

const TestModel = mongoose.model<Test>('Test', testSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await TestModel.create({ information: { isBug: true } });
}

run();
import * as mongoose from 'mongoose';

enum TestEnum{
  hello = "hi",
  bye = "good ridance"
}

interface Test {
  information: {
    isBug: TestEnum;
  }
}

const testSchema = new mongoose.Schema<Test>({
  information: {
    _id: false,
    isBug: {
      type: String,
      required: true,
      enum: TestEnum
    }
  }
});

const TestModel = mongoose.model<Test>('Test', testSchema);

async function run() {
  await mongoose.connect('mongodb://localhost:27017');
  await mongoose.connection.dropDatabase();

  await TestModel.create({ information: { isBug: true } });
}

run();

Also, it is worth mentioning that, in the contexts you mentioned, _id: false doesn't do anything. For example, in the following, _id: false is unnecessary because information is a nested path, not a subdocument. Subdocuments get _id by default (like top-level documents), nested paths do not.

const schema = new Schema<Test>({
  information: {
    _id: false, // <-- can remove this, no change in runtime behavior
    isBug: Boolean,
  },
})

In general, we advise against using _id: false in your schema definition, in favor of passing _id: false as a schema option. _id: false in schema definition is more of an unexpected quirk that we've maintained because it became too prevalent for us to disable without causing a lot of headache.

pauIbanez commented 6 months ago

Thank you for your information, I’ll update my typescript and give it a try.

However i don’t know about the _id thing. I’ll also try removing it, but from my experience sub paths generate id automatically if I don’t put that in there. Usually I do pass the _id false as a schema option when liking types to schemas, but as I said I’ve found that mested paths do generate ids when i don’t specify that _id:false.

I’ll try out both things and get back to you!

github-actions[bot] commented 6 months ago

This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 5 days

github-actions[bot] commented 6 months ago

This issue was closed because it has been inactive for 19 days and has been marked as stale.