microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.29k stars 12.39k forks source link

Range Error: Maximum call stack size exceeded while compiling our code #53179

Open bozmen opened 1 year ago

bozmen commented 1 year ago

Bug Report

🔎 Search Terms

tsc: RangeError: Maximum call stack size exceeded

🕗 Version & Regression Information

⏯ Playground Link

https://github.com/Klab-Berlin/mongoose-test

💻 Code

import mongoose from 'mongoose';

export type Schema<T> = {
  [key in keyof T]: TypeSchema<T[key]>
};

type TypeSchema<T> =
  T extends { type: (typeof mongoose.Schema.Types.String)[] } ? string[] :
  T extends { type: typeof mongoose.Schema.Types.String } ? string :
  T extends { type: (typeof mongoose.Schema.Types.Number)[] } ? number[] :
  T extends { type: typeof mongoose.Schema.Types.Number } ? number :
  T extends { type: (typeof mongoose.Schema.Types.Boolean)[] } ? boolean[] :
  T extends { type: typeof mongoose.Schema.Types.Boolean } ? boolean :
  T extends { type: any[] } ? any[] :
  T extends { type: any } ? any :
  Schema<T>;

/**
 * @template {mongoose.SchemaDefinition} T
 * @param {T} schemaBody
 * @param {mongoose.SchemaOptions} [options = {}]
 * @returns {import('./schema').Schema<T>}
 */
function createSchema(schemaBody, options = {}) {
  // @ts-ignore
  return new mongoose.Schema(schemaBody, options);
}

const { String, Number, Boolean, ObjectId, Mixed } = mongoose.Schema.Types;

export const userSchema = {
  someList: [
    createSchema(
      {
        id: { type: Number },
        paths: [
          createSchema(
            { path: { type: String }, type: { type: String } },
            { _id: false, id: false },
          ),
        ],
      },
      { _id: false, id: false },
    ),
  ],
};

tsconfig.json

{
  "compilerOptions": {
    "target": "es2015", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "lib": ["es6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    "module": "commonjs", /* Specify what module code is generated. */
    "rootDir": "src", /* Specify the root folder within your source files. */
    "resolveJsonModule": true, /* Enable importing .json files */
    "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    "checkJs": false, /* Enable error reporting in type-checked JavaScript files. */
    "outDir": "build/server", /* Specify an output folder for all emitted files. */
    "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
    "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
    "strict": true, /* Enable all strict type-checking options. */
    "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
    "skipLibCheck": true, /* Skip type checking all .d.ts files. */
    "removeComments": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build/**/*"]
}
// We can quickly address your report if:
//  - The code sample is short. Nearly all TypeScript bugs can be demonstrated in 20-30 lines of code!
//  - It doesn't use external libraries. These are often issues with the type definitions rather than TypeScript bugs.
//  - The incorrectness of the behavior is readily apparent from reading the sample.
// Reports are slower to investigate if:
//  - We have to pare too much extraneous code.
//  - We have to clone a large repo and validate that the problem isn't elsewhere.
//  - The sample is confusing or doesn't clearly demonstrate what's wrong.

🙁 Actual behavior

When we try to compile our code with tsc and with the given tsconfig file, we get the error tsc: RangeError: Maximum call stack size exceeded.

The whole error log:

running better-npm-run in $PROJECT_ROOT
Executing script: server:build

to be executed: tsc 
/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:95302
                throw e;
                ^

RangeError: Maximum call stack size exceeded
    at instantiateTypes (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51421:34)
    at getObjectTypeInstantiation (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51557:80)
    at instantiateTypeWorker (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51757:28)
    at instantiateTypeWithAlias (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51737:26)
    at instantiateType (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51723:37)
    at getConditionalType (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:50792:35)
    at getConditionalTypeInstantiation (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51715:25)
    at instantiateTypeWorker (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51789:24)
    at instantiateTypeWithAlias (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51737:26)
    at instantiateType (/$PROJECT_ROOT/node_modules/typescript/lib/tsc.js:51723:37)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! api@0.0.1 server:build: `bnr server:build`
npm ERR! Exit status 1
npm ERR! 
npm ERR! Failed at the api@0.0.1 server:build script.

🙂 Expected behavior

We expect to be able to compile this code. Not really sure if there is a problem with out types.

weswigham commented 1 year ago

Crash is caused by an infinite inference loop - when inferring the reverse mapped type between SchemaDefinitionProperty<undefined> (from mongoose) and Schema<T[key]> (a type made in the repro) we perform subtype reduction on the inference result (which is itself a bunch of reverse Schema mapped types over the various schema types), which, in turn, requests that we infer the reverse mapped type between SchemaDefinitionProperty<undefined> and Schema<T[key][key]>, which in turn... Yeah. We're pulling on the structure a bit too eagerly, causing us to go infinite. I can rework a perf improvement I made awhile back to give use a place to bail on the recursion, which should fix the issue, but it's a bit of an open question if we want it to error (and if so: where?), or would rather silently proceed despite the cut recursion (similarly to how recursive class references can work, but can result in some calculations witnessing unfinished type structure and thus producing inaccurate results).