The driver ships its own type declarations, so nothing extra is installed, and collection<T>() is where the types enter. Filters, updates, projections and results are then checked against T: insertOne takes OptionalId<T> (_id optional, the rest required), and a result is WithId<T> — T with _id mandatory, because the server always sends one back.
import { Collection, MongoClient, ObjectId, WithId, OptionalId } from "mongodb";
export interface Book {
_id?: ObjectId;
isbn: string; title: string; year: number; tags: string[];
}
const client = new MongoClient("mongodb://127.0.0.1:28111/?directConnection=true");
const books: Collection<Book> = client.db("bookshelf").collection<Book>("catalog");
export const add = (doc: OptionalId<Book>) => books.insertOne(doc);
export const newest = (): Promise<WithId<Book> | null> =>
books.findOne({}, { sort: { year: -1 } });
await books.find({ year: { $gt: "x" } }).toArray(); // these two lines are errors
(await newest())!.nope;
await books.findOne({ titel: "Dune" }); // but this one compilesbooks.ts(13,28): error TS2769: No overload matches this call.
The last overload gave the following error.
Type 'string' is not assignable to type 'number'.
books.ts(14,19): error TS2339: Property 'nope' does not exist on type 'WithId<Book>'.The checking goes deeper than field names: $gt: "x" fails because a comparison operator on year must hold a number, and $inc on a string field is rejected the same way. Know the limit, though — line 15 compiles. Filter<T> has to accept dot-notation keys such as "publisher.city", so a misspelled top-level field in a filter is legal TypeScript and matches nothing at runtime. The types catch shape errors in what you write, not typos in what you ask for. And nothing stops a document written last year from violating this year's interface, so validate at the boundary (Indexes and Query Performance) — the gap an ODM sets out to manage.