Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Maximum call stack size exceeded for complex type #2507

Closed
jgke opened this issue Feb 24, 2024 · 3 comments
Closed

Maximum call stack size exceeded for complex type #2507

jgke opened this issue Feb 24, 2024 · 3 comments
Labels
bug Functionality does not match expectation

Comments

@jgke
Copy link

jgke commented Feb 24, 2024

Search terms

Hi! I have a complex type generated with ts-proto, which is causing typedoc to crash with infinite recursion somewhere inside tsc.

Expected Behavior

Not crash.

Actual Behavior

$ npx typedoc
TypeDoc exiting with unexpected error:
RangeError: Maximum call stack size exceeded
    at typeToTypeNodeWorker (/path/to/project/node_modules/typescript/lib/typescript.js:49973:36)
    at typeToTypeNodeHelper (/path/to/project/node_modules/typescript/lib/typescript.js:49969:26)
    at typeToTypeNodeWorker (/path/to/project/node_modules/typescript/lib/typescript.js:50215:34)
    at typeToTypeNodeHelper (/path/to/project/node_modules/typescript/lib/typescript.js:49969:26)
[... repeating ...]

Steps to reproduce the bug

I tried to make this as minimal as possible. There's probably still something I managed to miss, but at least it's better than the original 540-line file.

export interface Struct {
    fields: { [key: string]: Value };
}

export interface Struct_FieldsEntry {
    value: Value | undefined;
}

export interface Value {
    structValue?: Struct | undefined;
    listValue?: ListValue | undefined;
}

export interface ListValue {
    values: Value[];
}

export function fromPartial<I extends Exact<DeepPartial<Struct_FieldsEntry>, I>>(object: I): Struct_FieldsEntry {
    return undefined as any;
};

type Builtin = undefined;

export type DeepPartial<T> = T extends Builtin ? T : Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
    ? P
    : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

Also available at TypeStrong/typedoc-repros#39

Environment

  • Typedoc version: 0.25.8
  • TypeScript version: 5.3.3
  • Node.js version: v18.17.0
  • OS: Ubuntu
@jgke jgke added the bug Functionality does not match expectation label Feb 24, 2024
@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 25, 2024

Thanks for the minimal reproduction! Getting it down this far will help a ton when figuring out what went wrong here. I suspect the recursive Exact is what's going to turn out to be the cause here.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 25, 2024

export interface Value {
    values: Value[];
}

export function fromPartial<I extends Exact<Value, I>>(object: I): void {
    throw 1;
}

type Exact<P, I extends P> = P extends P ? P & { [K in keyof P]: Exact<P[K], I[K]> } : never;

Further reduced, different infinite recursion in currently released version, but I suspect the same underlying cause.

@Gerrit0
Copy link
Collaborator

Gerrit0 commented Feb 26, 2024

After a couple hours spent digging into this... It's not a super easy fix unfortunately.

If you hover over the signature, you can see the type that TypeDoc is trying to convert.

image

Notice all the ..., where TypeScript decided to stop expanding the type as it is infinitely recursive. TypeDoc doesn't have a check for this. I've added one, which is controllable via --maxTypeConversionDepth, but it's really highlighting that I need to revisit how TypeDoc converts types. In general, I like that TypeDoc uses types rather than type nodes to convert types, since it means that library authors can do things like this:

type InternalOnly = true;
export type IfInternal<T, F> = InternalOnly extends true ? T : F;

function over(flag: 'a' | 'b'): string
function over(flag: IfInternal<never, string>): string
function over(flag: string): string { return flag }

Where over will accept any string if InternalOnly is changed to false (TypeDoc does this as a pre-publish step to make some signatures more lax), but will only accept 'a' | 'b' when set to true.

It is also nice for dealing with fully resolved type parameters, particularly with inheritance from generic classes.

// docs for MyList.at should show that it returns `string`, not `T`
class MyList extends Array<string> {}

There have been lots of issues requesting both that TypeDoc do more of this type resolution and that it do less of it... making it configurable is the obvious "solution"... if there was an easy way to do so, which there really isn't, at least that I've been able to come up with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Functionality does not match expectation
Projects
None yet
Development

No branches or pull requests

2 participants