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

tsc, tsserver: hangs with large union type and object spread in React HOC (strict mode) #29949

Closed
jgoz opened this issue Feb 18, 2019 · 20 comments · May be fixed by #53739
Closed

tsc, tsserver: hangs with large union type and object spread in React HOC (strict mode) #29949

jgoz opened this issue Feb 18, 2019 · 20 comments · May be fixed by #53739
Assignees
Labels
Bug A bug in TypeScript Domain: Big Unions The root cause is ultimately that big unions interact poorly with complex structures Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@jgoz
Copy link

jgoz commented Feb 18, 2019

TypeScript Version: 3.4.0-dev.20190216

Search Terms: hang higher order union strictFunctionTypes

Code

Note that this code has compilation errors, but that's not important. When invoking tsc or loading in an editor that uses tsserver, the compilation hangs/never completes.

import * as React from "react";

const animated: {
  [Tag in keyof JSX.IntrinsicElements]: React.ForwardRefExoticComponent<
    React.ComponentPropsWithRef<Tag>
  >
} = {};

function makeAnimated<T extends React.ReactType>(
  comp: T
): React.ForwardRefExoticComponent<React.ComponentPropsWithRef<T>> {
  return null as any; // not important
}

export interface UpgradedProps {
  show: boolean;
}

export function test<P>(
  component: React.ComponentType<P> | keyof React.ReactHTML
): React.ComponentType<P & UpgradedProps> {
  // changing to `const Comp: any` un-hangs tsserver
  const Comp =
    typeof component === "string"
      ? animated[component]
      : makeAnimated(component);

  return React.forwardRef<any, P & UpgradeProps>((props, ref) => {
    const { show, ...ownProps } = props; // addition of this line causes the hang
    return show ? <Comp {...ownProps} ref={ref} /> : null;
  });
}

Expected behavior:

Compilation completes (optionally with errors).

Actual behavior:

Compilation hangs.

Repro Link: https://github.com/jgoz/typescript-bug

Related Issues: Didn't find anything recent

@jgoz jgoz changed the title tsc, tsserver: hangs with large union type in React HOC tsc, tsserver: hangs with large union type and object spread in React HOC Feb 18, 2019
@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Feb 27, 2019
@RyanCavanaugh
Copy link
Member

With Version 3.4.0-dev.20190227:

D:\Throwaway\reactish>tsc a.tsx --diagnostics
a.tsx:3:7 - error TS2740: Type '{}' is missing the following properties from type '{ a: ForwardRefExoticComponent<Pick<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, "media" | "hidden" | "dir" | "slot" | "style" | "title" | "color" | "key" | "children" | ... 249 more ... | "onTransitionEndCapture"> & { ...; }>; ... 170 more ...; view: ForwardRefExoticComponent<...>; }': a, abbr, address, area, and 168 more.

3 const animated: {
        ~~~~~~~~

a.tsx:28:3 - error TS2322: Type 'ForwardRefExoticComponent<Pick<any, string | number | symbol> & RefAttributes<any>>' is not assignable to type 'ComponentType<P & UpgradedProps>'.
  Type 'ForwardRefExoticComponent<Pick<any, string | number | symbol> & RefAttributes<any>>' is not assignable to type 'FunctionComponent<P & UpgradedProps>'.
    Types of property 'defaultProps' are incompatible.
      Type 'Partial<Pick<any, string | number | symbol> & RefAttributes<any>>' has no properties in common with type 'Partial<P & UpgradedProps>'.

28   return React.forwardRef<any, P & UpgradeProps>((props, ref) => {
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29     const { show, ...ownProps } = props; // addition of this line causes the hang
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30     return show ? <Comp {...ownProps} ref={ref} /> : null;
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
31   });
   ~~~~~

a.tsx:28:36 - error TS2304: Cannot find name 'UpgradeProps'.

28   return React.forwardRef<any, P & UpgradeProps>((props, ref) => {
                                      ~~~~~~~~~~~~

a.tsx:30:19 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

30     return show ? <Comp {...ownProps} ref={ref} /> : null;
                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


Found 4 errors.

Files:            13
Lines:         65254
Nodes:        211336
Identifiers:   67355
Symbols:      148609
Types:         58118
Memory used: 174251K
I/O read:      0.02s
I/O write:     0.00s
Parse time:    0.50s
Bind time:     0.22s
Check time:    3.50s
Emit time:     0.03s
Total time:    4.25s

With --skipLibCheck (highly recommended), total time is 1.44s. Can you try with latest and see what you see?

@jgoz
Copy link
Author

jgoz commented Feb 27, 2019

@RyanCavanaugh With Version 3.4.0-dev.20190227, tsc does complete but tsserver still hangs. This can be observed in VS Code by hovering the mouse over animated or makeAnimated - it just shows Loading... and never changes.

I do have "skipLibCheck": true in my tsconfig.

I tweaked the example to remove compilation errors:

import * as React from "react";

const animated: {
  [Tag in keyof JSX.IntrinsicElements]: React.ForwardRefExoticComponent<
    React.ComponentPropsWithRef<Tag>
  >
} = {} as any;

function makeAnimated<T extends React.ReactType>(
  _comp: T
): React.ForwardRefExoticComponent<React.ComponentPropsWithRef<T>> {
  return null as any; // not important
}

export interface UpgradedProps {
  show: boolean;
}

export function test<P>(
  component: React.ComponentType<P> | keyof React.ReactHTML
): any {
  // --> changing to `const Comp: any` un-hangs tsserver <--
  const Comp =
    typeof component === "string"
      ? animated[component]
      : makeAnimated(component);

  return React.forwardRef<any, P & UpgradedProps>((props, ref) => {
    const { show, ...ownProps } = props;
    return show ? <Comp {...ownProps} ref={ref} /> : null;
  });
}

@RyanCavanaugh
Copy link
Member

image

Not sure what's going on! If you can get a debugger attached to tsserver that may be helpful, though it's hard to say since this is a pretty small repro and we really should be seeing the same results.

@jgoz
Copy link
Author

jgoz commented Feb 27, 2019

Here is my tsconfig for reference:

{
  "compileOnSave": false,
  "compilerOptions": {
    "importHelpers": true,
    "jsx": "react",
    "lib": [
      "dom",
      "es5",
      "es2015.collection",
      "es2015.iterable",
      "es2015.promise",
      "es2015.symbol"
    ],
    "module": "commonjs",
    "moduleResolution": "node",
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "preserveSymlinks": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es5"
  }
}

@jgoz
Copy link
Author

jgoz commented Feb 27, 2019

@RyanCavanaugh I just disabled all options and re-enabled one-by-one. The server will hang when strict: true is set.

{
  "compileOnSave": false,
  "compilerOptions": {
    "jsx": "react",
    "skipLibCheck": true,
    "strict": true
  }
}

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Needs More Info The issue still hasn't been fully clarified labels Feb 27, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.4.0 milestone Feb 27, 2019
@RyanCavanaugh
Copy link
Member

There's the trick! Thank you

@jgoz jgoz changed the title tsc, tsserver: hangs with large union type and object spread in React HOC tsc, tsserver: hangs with large union type and object spread in React HOC (strict mode) Feb 27, 2019
@sheetalkamat
Copy link
Member

Pretty sure this is to do with type string genaration.

@weswigham
Copy link
Member

Looks like the specific strict flag that's in the mix here is strictFunctionTypes.

@jchitel
Copy link

jchitel commented Mar 18, 2019

I created this a few days ago without doing much investigation, but just today discovered that it is caused by strictFunctionTypes. Perhaps it's the same problem?

@weswigham
Copy link
Member

Maybe - #30411 may also help.

Spent some time looking at this the other day, though (and talking about it in our design meeting) - ultimately the ref field in react, when materialized, currently necessitates making a union of 2^111 members. Naturally, we cannot actually do that. The reason for that is that the function type-ref and the object type-ref are not mutually exclusive - so when 111 union-pairs of them are intersected together, we have to form the powerset of alternatives that could be in the resulting union. If we could recognize {(ref: T): void; currentRef?: undefined} and {readonly currentRef: T} as mutually exclusive (we do not currently), and redefine the function ref to be along the lines of {(ref: T): void; currentRef?: undefined}, then we could simplify many of the alternatives as we go when we actually construct the type (and have a resulting union that only has around 222 members).

@jgoz
Copy link
Author

jgoz commented Mar 18, 2019

@weswigham Function refs do not have a current property, so function and object refs should be mutually exclusive. This is how they're defined in the react typings:

interface RefObject<T> {
    readonly current: T | null;
}

type Ref<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"] | RefObject<T> | null;

interface RefAttributes<T> extends Attributes {
    ref?: Ref<T>;
}

Or is there some other definition you are referring to where they do overlap?

@weswigham
Copy link
Member

weswigham commented Mar 18, 2019

Function refs do not have a current property

They don't define one, however as written a {(instance: T | null): void; readonly current: T | null} would actually be assignable to a { bivarianceHack(instance: T | null): void }["bivarianceHack"], and to a RefObject<T>. However, were the function type defined as type FunctionRef<T> = { bivarianceHack(instance: T | null): void }["bivarianceHack"] & { current?: undefined } it would be impossible for a type to be both a RefObject and a RefFunction sans fields of type never, which would themselves imply the containing type can't actually exist (we don't currently detect this, which is why it'd also need some changes in TS).

@jgoz
Copy link
Author

jgoz commented Mar 18, 2019

Ah, makes sense, thank you for the explanation!

@ahrbil
Copy link

ahrbil commented Feb 6, 2020

is there any other solutions or workarounds for this?
for me turning off

"strict": false,
"strictNullChecks": false,
"strictPropertyInitialization": false

fixed it

@weswigham
Copy link
Member

#42772 may still be valuable enough to revive at some point if operating close to our limits is truly common, but we've already done a lot of recentish work to defer/simplify unions like these wherever possible, and minimally, we should issue a complexity error rather than hang nowadays.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Domain: Big Unions The root cause is ultimately that big unions interact poorly with complex structures Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet