From 15c9f845817c8de6ddcb53f15ab3a6e5987035c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:13:28 +0000 Subject: [PATCH 1/4] Initial plan From 811dafa848c7fd340751cd2aa80a28db6a8d332f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:17:03 +0000 Subject: [PATCH 2/4] Fix return types: findOne and cursor methods now return T instead of Partial Co-authored-by: semics-tech <47321195+semics-tech@users.noreply.github.com> --- src/collection.ts | 2 +- src/cursors/findCursor.ts | 8 ++--- test-type-fix.ts | 69 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 test-type-fix.ts diff --git a/src/collection.ts b/src/collection.ts index 2d6e217..0dff0d7 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -388,7 +388,7 @@ export class MongoLiteCollection { * @param projection Optional. Specifies the fields to return. * @returns {Promise} The found document or `null`. */ - async findOne(filter: Filter, projection?: Projection): Promise | null> { + async findOne(filter: Filter, projection?: Projection): Promise { await this.ensureTable(); const cursor = this.find(filter).limit(1); if (projection) { diff --git a/src/cursors/findCursor.ts b/src/cursors/findCursor.ts index 4918ed3..cc5212f 100644 --- a/src/cursors/findCursor.ts +++ b/src/cursors/findCursor.ts @@ -497,7 +497,7 @@ export class FindCursor { return this; } - private applyProjection(doc: T): Partial { + private applyProjection(doc: T): T { if (!this.projectionFields) return doc; const projectedDoc: Partial = {}; @@ -605,14 +605,14 @@ export class FindCursor { } } } - return projectedDoc; + return projectedDoc as T; } /** * Executes the query and returns all matching documents as an array. * @returns A promise that resolves to an array of documents. */ - public async toArray(): Promise[]> { + public async toArray(): Promise { let finalSql = this.queryParts.sql; const finalParams = [...this.queryParts.params]; @@ -658,7 +658,7 @@ export class FindCursor { * @returns A promise that resolves to the first matching document or null if no matches are found. * @throws Error if the query fails. */ - public async first(): Promise | null> { + public async first(): Promise { const results = await this.limit(1).toArray(); return results.length > 0 ? results[0] : null; } diff --git a/test-type-fix.ts b/test-type-fix.ts new file mode 100644 index 0000000..d847761 --- /dev/null +++ b/test-type-fix.ts @@ -0,0 +1,69 @@ +// Quick test to verify that findOne returns full type T instead of Partial +import { MongoLite, DocumentWithId } from './src/index.js'; + +interface User extends DocumentWithId { + name: string; + email: string; + age: number; +} + +async function testTypeFix() { + const db = new MongoLite(':memory:'); + await db.connect(); + const users = db.collection('users'); + + // Insert a test user + await users.insertOne({ + name: 'Test User', + email: 'test@example.com', + age: 25, + }); + + // Test 1: findOne should return User | null (not Partial | null) + const user = await users.findOne({ name: 'Test User' }); + if (user) { + // These properties should all be available without TypeScript complaining + console.log('✅ Test 1: findOne returns full type'); + console.log(' _id:', user._id); + console.log(' name:', user.name); + console.log(' email:', user.email); + console.log(' age:', user.age); + } + + // Test 2: find().toArray() should return User[] (not Partial[]) + const allUsers = await users.find({}).toArray(); + if (allUsers.length > 0) { + const firstUser = allUsers[0]; + console.log('✅ Test 2: find().toArray() returns full type'); + console.log(' _id:', firstUser._id); + console.log(' name:', firstUser.name); + console.log(' email:', firstUser.email); + console.log(' age:', firstUser.age); + } + + // Test 3: find().first() should return User | null (not Partial | null) + const firstUserFromCursor = await users.find({}).first(); + if (firstUserFromCursor) { + console.log('✅ Test 3: find().first() returns full type'); + console.log(' _id:', firstUserFromCursor._id); + console.log(' name:', firstUserFromCursor.name); + console.log(' email:', firstUserFromCursor.email); + console.log(' age:', firstUserFromCursor.age); + } + + // Test 4: With projection, the return type is still T but some fields may be undefined + // This is acceptable since TypeScript's structural typing allows this + const userWithProjection = await users.findOne({ name: 'Test User' }, { name: 1 }); + if (userWithProjection) { + console.log('✅ Test 4: findOne with projection returns T (but fields may be undefined at runtime)'); + console.log(' _id:', userWithProjection._id); + console.log(' name:', userWithProjection.name); + console.log(' email:', userWithProjection.email, '(should be undefined)'); + console.log(' age:', userWithProjection.age, '(should be undefined)'); + } + + await db.close(); + console.log('\n✅ All type tests passed!'); +} + +testTypeFix().catch(console.error); From d613b5391082980a25ff35b318d69b44af7a35a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:17:16 +0000 Subject: [PATCH 3/4] Remove test file after validation Co-authored-by: semics-tech <47321195+semics-tech@users.noreply.github.com> --- test-type-fix.ts | 69 ------------------------------------------------ 1 file changed, 69 deletions(-) delete mode 100644 test-type-fix.ts diff --git a/test-type-fix.ts b/test-type-fix.ts deleted file mode 100644 index d847761..0000000 --- a/test-type-fix.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Quick test to verify that findOne returns full type T instead of Partial -import { MongoLite, DocumentWithId } from './src/index.js'; - -interface User extends DocumentWithId { - name: string; - email: string; - age: number; -} - -async function testTypeFix() { - const db = new MongoLite(':memory:'); - await db.connect(); - const users = db.collection('users'); - - // Insert a test user - await users.insertOne({ - name: 'Test User', - email: 'test@example.com', - age: 25, - }); - - // Test 1: findOne should return User | null (not Partial | null) - const user = await users.findOne({ name: 'Test User' }); - if (user) { - // These properties should all be available without TypeScript complaining - console.log('✅ Test 1: findOne returns full type'); - console.log(' _id:', user._id); - console.log(' name:', user.name); - console.log(' email:', user.email); - console.log(' age:', user.age); - } - - // Test 2: find().toArray() should return User[] (not Partial[]) - const allUsers = await users.find({}).toArray(); - if (allUsers.length > 0) { - const firstUser = allUsers[0]; - console.log('✅ Test 2: find().toArray() returns full type'); - console.log(' _id:', firstUser._id); - console.log(' name:', firstUser.name); - console.log(' email:', firstUser.email); - console.log(' age:', firstUser.age); - } - - // Test 3: find().first() should return User | null (not Partial | null) - const firstUserFromCursor = await users.find({}).first(); - if (firstUserFromCursor) { - console.log('✅ Test 3: find().first() returns full type'); - console.log(' _id:', firstUserFromCursor._id); - console.log(' name:', firstUserFromCursor.name); - console.log(' email:', firstUserFromCursor.email); - console.log(' age:', firstUserFromCursor.age); - } - - // Test 4: With projection, the return type is still T but some fields may be undefined - // This is acceptable since TypeScript's structural typing allows this - const userWithProjection = await users.findOne({ name: 'Test User' }, { name: 1 }); - if (userWithProjection) { - console.log('✅ Test 4: findOne with projection returns T (but fields may be undefined at runtime)'); - console.log(' _id:', userWithProjection._id); - console.log(' name:', userWithProjection.name); - console.log(' email:', userWithProjection.email, '(should be undefined)'); - console.log(' age:', userWithProjection.age, '(should be undefined)'); - } - - await db.close(); - console.log('\n✅ All type tests passed!'); -} - -testTypeFix().catch(console.error); From 8d0aba14c837c487f6691f7ebea8fae8ef7fb8bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 19:18:11 +0000 Subject: [PATCH 4/4] Add documentation explaining type assertion behavior with projections Co-authored-by: semics-tech <47321195+semics-tech@users.noreply.github.com> --- src/collection.ts | 3 +++ src/cursors/findCursor.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/collection.ts b/src/collection.ts index 0dff0d7..6666d8f 100644 --- a/src/collection.ts +++ b/src/collection.ts @@ -387,6 +387,9 @@ export class MongoLiteCollection { * @param filter The query criteria. * @param projection Optional. Specifies the fields to return. * @returns {Promise} The found document or `null`. + * @remarks When using a projection, some fields may be undefined at runtime despite the + * return type being `T`. This provides better ergonomics for the common case where no + * projection is used. */ async findOne(filter: Filter, projection?: Projection): Promise { await this.ensureTable(); diff --git a/src/cursors/findCursor.ts b/src/cursors/findCursor.ts index cc5212f..42c69be 100644 --- a/src/cursors/findCursor.ts +++ b/src/cursors/findCursor.ts @@ -491,12 +491,23 @@ export class FindCursor { * @param projection An object where keys are field names and values are 1 (include) or 0 (exclude). * `_id` is included by default unless explicitly excluded. * @returns The `FindCursor` instance for chaining. + * @remarks When using projections, the returned documents may have undefined fields at runtime, + * though the TypeScript type will still be `T`. Consumers should be aware that projected-out + * fields will be undefined. */ public project(projection: Projection): this { this.projectionFields = projection; return this; } + /** + * Applies projection to a document, returning only the specified fields. + * @param doc The full document to apply projection to + * @returns The projected document. When a projection is applied, some fields may be undefined + * at runtime despite the return type being `T`. This trade-off provides better developer + * experience for the common case (no projection) while maintaining type compatibility. + * @private + */ private applyProjection(doc: T): T { if (!this.projectionFields) return doc; @@ -605,6 +616,9 @@ export class FindCursor { } } } + // Type assertion: We return T instead of Partial for better developer experience. + // When no projection is used, this is accurate. When a projection is used, consumers + // should be aware that some fields may be undefined at runtime. return projectedDoc as T; }