I think it depends on your implementation.

For example, I tend to use Mongoose as the interface to Mongo, with which you're required to set a Schema for each collection of documents. You can easily set every single field type to `Mixed` (analogous to `any`, `auto`, etc), but if you're the least bit disciplined you can set any fields to any variation of:

* String
* Number
* Date
* Buffer
* Boolean
* Mixed
* ObjectId
* Array
* Decimal128
* Map

(http://mongoosejs.com/docs/guide.html)

And there are TypeScript typings supplied for Mongoose, so I typically recreate the models as TS interfaces for wherever I use them in other parts of my code. It's extremely intuitive and quick to develop. It's a tad redundant defining the Schema types, and then a TS interface, but I usually don't mind. I include them in the same file for coherence so that I'm always importing from the same module for anything relating to the model.

For (a basic) example:

/* MyCollection.ts */

import {
Document,
Model,
model,
Schema,
Types
} from 'mongoose';

export interface IMySchemaFields
{
_id?: Types.ObjectId; // †
name: string;
age?: number;
fungi: boolean;
ET?: "phone home" | "owwww";
}

export type TMySchemaDocument = Document & IMySchemaFields;

export type TMySchemaModel = Model<TMySchemaDocument>;

export const MySchema: Schema = new Schema(
{
_id: Schema.Types.ObjectId // † NOTE
name: String,
age: Number,
fungi: Boolean,
ET: String,
});

export const MyCollection: TMySchemaModel = model('myCollection', MySchema);

† NOTE: there are a couple of quirks. Mongoose uses constructors to define types whereas TypeScript types are a different symbol understood only by the compiler— so in this case they use different aliases of the same object...

So when I move to make any queries via the ODM, I import and use the associated types. The types won't allow you to reference fields that don't exist on the schema that way. You can force it by cheating the compiler and casting any arbitrary non-existent field to `<any>`, but I try to reserve that for hungover POC cases. It does suggest immutability of the object received from the query, but again— there are many ways to ignore that if you wanted to, or just didn't care.

Example ODM query:

/* MyController.ts */

import * as mongoose from 'mongoose';
import {
MyCollection,
IMySchemaFields
} from '../models/MyCollection.ts'

/* ... pretend there's relevant stuff happening here */

MyCollection.findOne({ _id: '436rgerr25gw4gfdhdsf', },
(error: Error, collection: IMyCollectionFields, }) : any => {

// Do something with your `collection` object.
// Just don't try to access, modify, or add fields
// that don't exist on the referenced interface.
});

/* More things probably happen here */

As for TypeScript for a large project... I think we get into different kinds of discussions here (eg, is Node or Deno the right tool, etc etc). The kind of applications I've used this design on have not had to face significant scaling or load problems. They're very applied circumstances that face small-to-medium/large traffic. I haven't benchmarked for anything that operated on a real significant scale. We will be testing for around the ~1k-2k concurrent user mark later this summer. But that doesn't involve consistent read/writes— it will involve regular reads, but seldom writes. I might try to write about it later, but it's kind of a fast-track thing so I'm a bit buried in it.

Other projects following a similar design tend to be internal tooling for editorial and other teams in media and so don't see significant traffic or structures/changes beyond initial versions.

I can understand many peoples' reservations and concerns. I definitely think it's a matter of what's right for the job, considering the circumstances and resources available.