Skip to content

Conversation

ssalbdivad
Copy link

@ssalbdivad ssalbdivad commented Aug 16, 2025

Summary

This pull request optimizes many of the types transitively referenced from InferRawDocType.

The goal is purely to improve type performance without changing the behavior of the types themselves.

It also adds some additional tests for InferRawDocType and some type-level benchmarks that use @ark/attest to granularly measure type instantiations for a few InferRawDocType calls, which were reduced by an average of 50% by this PR.

Formatting

I wasn't sure how to handle formatting for the types in this repo, so I tried to create a prettier config that was similar to the formatting I found:

{
  "printWidth": 120,
  "trailingComma": "none",
  "singleQuote": true,
  // helps flatten type-level ternaries, significantly increasing readability
  "experimentalTernaries": true
}

I only applied this formatting to .d.ts files that I significantly changed.

If there is a pre-existing tool I can use to format the types I added, I'd be happy to update my PR to use that.

Runtime Changes

There is only one new line of runtime logic, adding a schemaName property to SchemaType instances, mirroring the one that already exists on their constructors.

This helps discriminate schema instances from one another so the types can be compared more efficiently without IfEquals and could be useful at runtime as well.

If the team would prefer, I could also remove the runtime component and make this a private property so that it would still make schema types incompatible for more efficient comparisons without adding a new property visible to consumers at a type-level or at runtime.

Follow-ups

I notice many of the types e.g. InferSchemaType closely parallel the types I optimized in InferRawDocType. If the maintainers are interested in further such optimizations, similar strategies and benchmarking could likely be applied to many of the repo's types for even more significant improvements.

@vkarpov15 vkarpov15 requested a review from Copilot August 16, 2025 21:02
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request optimizes the TypeScript types transitively referenced from InferRawDocType to improve type performance by an average of 50% without changing behavior. The optimization adds schema name discrimination for more efficient type comparisons and includes comprehensive benchmarking.

  • Adds schemaName property to SchemaType instances for efficient schema discrimination
  • Introduces type-level benchmarks using @ark/attest to measure type instantiation performance
  • Expands test coverage for InferRawDocType with additional test cases for optionality, timestamps, and various definition types

Reviewed Changes

Copilot reviewed 5 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/types/inferrawdoctype.test.ts Adds comprehensive test cases for InferRawDocType covering optionality, timestamps, and various schema definition types
test/schemaname.test.js Tests the new schemaName property on both constructor and instance level
package.json Adds @ark/attest dependency and new benchmark script for type performance testing
lib/schemaType.js Adds schemaName property to SchemaType instances mirroring the constructor property
benchmarks/typescript/infer.bench.mts Implements type-level benchmarks measuring InferRawDocType performance improvements

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

const start = require('./common');
const Schema = start.mongoose.Schema;

describe.only('schemaname', function() {
Copy link
Preview

Copilot AI Aug 16, 2025

Choose a reason for hiding this comment

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

The test suite uses describe.only which will cause only this test to run, skipping all other tests in the codebase. This should be changed to describe to prevent accidentally disabling other tests.

Suggested change
describe.only('schemaname', function() {
describe('schemaname', function() {

Copilot uses AI. Check for mistakes.

@vkarpov15
Copy link
Collaborator

Thanks for the very detailed PR, this looks very cool! I need to try attest - my current TypeScript perf optimization workflow is quite painful. I'm fine with the runtime changes, and I'll look at the formatting changes some more - right now Mongoose doesn't have an automated formatter so your formatting changes are not wrong.

However, it looks like npm run test-tsd fails with a couple of errors here: https://github.com/Automattic/mongoose/actions/runs/17012866137/job/48231243428?pr=15588

 > [email protected] test-tsd
> node ./test/types/check-types-filename && tsd


  test/types/schema.test.ts:1644:2
  ✖   626:20  Property name does not exist on type String | Boolean | BooleanConstructor | Function[] | typeof SchemaType | typeof Boolean | Schema.Types.Boolean | typeof Mixed | ... 5 more ... | boolean[].
  Property name does not exist on type String.                                                                                                                                          
  ✖  1273:22  Argument of type String | StringConstructor | Function[] | typeof String | Schema.Types.String | typeof SchemaType | typeof Mixed | Schema<any, any, any, ... 5 more ..., Document<...> & ... 2 more ... & { ...; }> | ... 4 more ... | MixedSchemaTypeOptions<...> is not assignable to parameter of type IUser.
  Property name is missing in type String but required in type IUser.  
  ✖  1276:22  Argument of type String | StringConstructor | Function[] | typeof String | Schema.Types.String | typeof SchemaType | typeof Mixed | Schema<any, any, any, ... 5 more ..., Document<...> & ... 2 more ... & { ...; }> | ... 4 more ... | MixedSchemaTypeOptions<...> is not assignable to parameter of type IUser.
  Property name is missing in type String but required in type IUser.  
  ✖  1644:2   Parameter type User is not identical to argument type { userName: string; date: NativeDate; }.                                                                                                                                                                                                                                                                                          

  test/types/inferrawdoctype.test.ts:78:2
  ✖    78:2   Parameter type { foo: true; } & { bar: NativeDate; } is not identical to argument type { foo: true; bar: NativeDate; }.                                                                                                                                                                                                                                                                 

  5 errors

Any idea what might be causing those?

@ssalbdivad
Copy link
Author

@vkarpov15 Whoops, sorry I missed that script!

Most of these issues were just due to the new intersections being simplified to not include & and instead have flattened properties which should be an improvement.

There was one tricky issue with the this parameter that I found a workaround for.

Let me know if you have any questions!

@ssalbdivad
Copy link
Author

ssalbdivad commented Aug 22, 2025

@vkarpov15 Just updated this with the changes from 8.18.

I've also tested it across Vanta's monorepo which uses defines hundreds of schemas using Mongoose and resolved the only error which was an infinite depth error inferring any.

Let me know what a path would look like to getting this merged!

@vkarpov15 vkarpov15 added this to the 8.19 milestone Aug 25, 2025
@vkarpov15
Copy link
Collaborator

Thanks! I'll re-review and target merging this for 8.19. Do you work at Vanta?

// can be efficiently checked like:
// `[T] extends [neverOrAny] ? T : ...`
// to avoid edge cases
type neverOrAny = ' ~neverOrAny~';
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd prefer to use Mongoose's IfAny<> type here if possible, this check seems a bit brittle, bit this is mostly a pedantic concern, not blocking.

Copy link
Author

Choose a reason for hiding this comment

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

In addition to the check being slightly more performant, the main reason I'd recommend avoiding IfThen style utilities in place of conditional types is that all expressions passed to these utilities must be fully evaluated when the type is instantiated.

On the other hand, when you have a conditional type like this one, or even slightly less efficiently if you were to use something like IsAny<T> extends true ? ... : ..., only the matching branch would be evaluated.

This utility is based on the version I use throughout ArkType. It is definitely not fragile, but if you prefer something like IsAny that could also be a fine solution.

/** Can be used to test for the universal subtypes, `any` and `never`, e.g.:
 *
 * ```ts
 * type isAnyOrNever<t> = [t] extends [anyOrNever] ? true : false
 * ```
 *
 *  The actual value is a string literal, but the only realistic subtypes
 *  of that literal are `any` and `never`.
 */
export type anyOrNever = " anyOrNever"

@vkarpov15
Copy link
Collaborator

It is a little strange that this PR causes there to be more TypeScript instantiations and more TypeScript memory usage rather than less. Any ideas why that is?

Stats from 8.18 release: https://github.com/Automattic/mongoose/actions/runs/17157925968/job/48679431153


> benchmark
> tsc --extendedDiagnostics

Files:                         196
Lines of Library:            40506
Lines of Definitions:        79119
Lines of TypeScript:            20
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                107987
Symbols:                    263552
Types:                       90938
Instantiations:             294819
Memory used:               285960K
Assignability cache size:    33690
Identity cache size:           144
Subtype cache size:              3
Strict subtype cache size:       4
I/O Read time:               0.02s
Parse time:                  0.46s
ResolveModule time:          0.04s
ResolveTypeReference time:   0.01s
ResolveLibrary time:         0.01s
Program time:                0.60s
Bind time:                   0.23s
Check time:                  3.19s
transformTime time:          0.01s
commentTime time:            0.00s
I/O Write time:              0.00s
printTime time:              0.01s
Emit time:                   0.01s
Total time:                  4.04s

Latest stats from this PR: https://github.com/Automattic/mongoose/actions/runs/17164018210/job/48824005687?pr=15588

 > tsc --extendedDiagnostics

Files:                         196
Lines of Library:            40506
Lines of Definitions:        79216
Lines of TypeScript:            20
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                107925
Symbols:                    264536
Types:                       91969
Instantiations:             297521
Memory used:               286803K
Assignability cache size:    33932
Identity cache size:           145
Subtype cache size:              3
Strict subtype cache size:       4
I/O Read time:               0.02s
Parse time:                  0.43s
ResolveModule time:          0.04s
ResolveTypeReference time:   0.01s
ResolveLibrary time:         0.02s
Program time:                0.56s
Bind time:                   0.23s
Check time:                  2.77s
transformTime time:          0.01s
commentTime time:            0.00s
I/O Write time:              0.00s
printTime time:              0.01s
Emit time:                   0.01s
Total time:                  3.58s

@ssalbdivad
Copy link
Author

ssalbdivad commented Aug 25, 2025

It is a little strange that this PR causes there to be more TypeScript instantiations and more TypeScript memory usage rather than less. Any ideas why that is?

The reason for this is likely just the new tests and benchmarks that were added as a part of this PR.

Isolated benchmarks like those from @ark/attest that were added in this PR or check times against large, static expressions in an isolated environment are the best way to evaluate relative changes for something like this.

Thanks! I'll re-review and target merging this for 8.19. Do you work at Vanta?

Awesome! I am currently doing some consulting work for Vanta to improve their type performance.

@vkarpov15 vkarpov15 changed the base branch from master to 8.19 August 25, 2025 21:20
@vkarpov15
Copy link
Collaborator

I'll merge this into 8.19 branch for now so I can tinker with it a bit more, but overall I think this PR is excellent 👍 I'd just like to take a closer look as to why the top-line instantiations and memory usage are slightly higher.

@vkarpov15 vkarpov15 merged commit ef7bada into Automattic:8.19 Aug 25, 2025
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants