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

types: improve the typing of FilterQuery<T> type to prevent it from only getting typed to any #14436

Merged
merged 1 commit into from Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/tsc-diagnostics-check.js
Expand Up @@ -3,7 +3,7 @@
const fs = require('fs');

const stdin = fs.readFileSync(0).toString('utf8');
const maxInstantiations = isNaN(process.argv[2]) ? 120000 : parseInt(process.argv[2], 10);
const maxInstantiations = isNaN(process.argv[2]) ? 130000 : parseInt(process.argv[2], 10);

console.log(stdin);

Expand Down
36 changes: 28 additions & 8 deletions test/types/queries.test.ts
@@ -1,4 +1,5 @@
import {
Condition,
HydratedDocument,
Schema,
model,
Expand Down Expand Up @@ -84,10 +85,15 @@ Test.find({ parent: { $in: ['0'.repeat(24)] } });
Test.find({ name: { $in: ['Test'] } }).exec().then((res: Array<ITest>) => console.log(res));
Test.find({ tags: 'test' }).exec();
Test.find({ tags: { $in: ['test'] } }).exec();
Test.find({ tags: /test/ }).exec();
Test.find({ tags: { $in: [/test/] } }).exec();

// Implicit `$in`
Test.find({ name: ['Test1', 'Test2'] }).exec();

// Implicit `$in` for regex string
Test.find({ name: [/Test1/, /Test2/] });

Test.find({ name: 'test' }, (err: Error | null, docs: ITest[]) => {
console.log(!!err, docs[0].age);
});
Expand Down Expand Up @@ -307,20 +313,34 @@ function autoTypedQuery() {
}

function gh11964() {
class Repository<T extends { id: string }> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can make / have a helper that is a BasicPojoDocument for easy / quick / shared access

find(id: string) {
const idCondition: Condition<T['id']> = id as Condition<T['id']>;

// `as` is necessary because `T` can be `{ id: never }`,
// so we need to explicitly coerce
const filter: FilterQuery<T> = { id } as FilterQuery<T>;
vkarpov15 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

function gh14397() {
type Condition<T> = ApplyBasicQueryCasting<T> | QuerySelector<ApplyBasicQueryCasting<T>>; // redefined here because it's not exported by mongoose

type WithId<T extends object> = T & { id: string };

class Repository<T extends object> {
/* ... */
type TestUser = {
name: string;
age: number;
};

find(id: string) {
const idCondition: Condition<WithId<T>>['id'] = id; // error :(
const filter: FilterQuery<WithId<T>> = { id }; // error :(
const id: string = 'Test Id';

/* ... */
}
}
let idCondition: Condition<WithId<TestUser>['id']>;
let filter: FilterQuery<WithId<TestUser>>;

expectAssignable<typeof idCondition>(id);
expectAssignable<typeof filter>({ id });
}

function gh12091() {
Expand Down
22 changes: 17 additions & 5 deletions types/query.d.ts
@@ -1,8 +1,20 @@
declare module 'mongoose' {
import mongodb = require('mongodb');

export type ApplyBasicQueryCasting<T> = T | T[] | (T extends (infer U)[] ? U : any) | any;
type Condition<T> = ApplyBasicQueryCasting<T> | QuerySelector<ApplyBasicQueryCasting<T>>;
type StringQueryTypeCasting = string | RegExp;
type ObjectIdQueryTypeCasting = Types.ObjectId | string;
type UUIDQueryTypeCasting = Types.UUID | string;

type QueryTypeCasting<T> = T extends string
? StringQueryTypeCasting
: T extends Types.ObjectId
? ObjectIdQueryTypeCasting
: T extends Types.UUID
? UUIDQueryTypeCasting
: T | any;

export type ApplyBasicQueryCasting<T> = T | T[] | (T extends (infer U)[] ? QueryTypeCasting<U> : T);
export type Condition<T> = ApplyBasicQueryCasting<QueryTypeCasting<T>> | QuerySelector<ApplyBasicQueryCasting<QueryTypeCasting<T>>>;

type _FilterQuery<T> = {
[P in keyof T]?: Condition<T[P]>;
Expand Down Expand Up @@ -385,7 +397,7 @@ declare module 'mongoose' {
): QueryWithHelpers<Array<DocType>, DocType, THelpers, RawDocType, 'find'>;
find(
filter: FilterQuery<RawDocType>
): QueryWithHelpers<Array<RawDocType>, DocType, THelpers, RawDocType, 'find'>;
): QueryWithHelpers<Array<DocType>, DocType, THelpers, RawDocType, 'find'>;
find(): QueryWithHelpers<Array<DocType>, DocType, THelpers, RawDocType, 'find'>;

/** Declares the query a findOne operation. When executed, returns the first found document. */
Expand Down Expand Up @@ -481,7 +493,7 @@ declare module 'mongoose' {
get(path: string): any;

/** Returns the current query filter (also known as conditions) as a POJO. */
getFilter(): FilterQuery<RawDocType>;
getFilter(): FilterQuery<DocType>;

/** Gets query options. */
getOptions(): QueryOptions<DocType>;
Expand All @@ -490,7 +502,7 @@ declare module 'mongoose' {
getPopulatedPaths(): Array<string>;

/** Returns the current query filter. Equivalent to `getFilter()`. */
getQuery(): FilterQuery<RawDocType>;
getQuery(): FilterQuery<DocType>;

/** Returns the current update operations as a JSON object. */
getUpdate(): UpdateQuery<DocType> | UpdateWithAggregationPipeline | null;
Expand Down