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

“Type instantiation is excessively deep and possibly infinite” but only in a large codebase #34933

Open
karol-majewski opened this issue Nov 6, 2019 · 53 comments · May be fixed by #44997
Open
Assignees
Labels
Bug A bug in TypeScript Domain: Big Unions The root cause is ultimately that big unions interact poorly with complex structures Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@karol-majewski
Copy link

karol-majewski commented Nov 6, 2019

TypeScript Version: 3.7.2, 3.8.0-dev.20191102 (worked in 3.6)

Search Terms:

  • Type instantiation is excessively deep and possibly infinite.ts(2589)
  • Mapped types
  • Generics
  • Conditional types

Code

Note: this issue manifests itself only in our codebase. When you run the same code in TypeScript Playground, it seems to be working fine.

The snippet is hardly minimal, but I reduced it as much as I could. I recorded a video where exactly the same code yields an error different than the one in TypeScript Playground. I tried with two versions of TypeScript: 3.7.2 and 3.8.0-dev.20191102. It worked correctly with 3.6.

Since @sheetalkamat and @DanielRosenwasser have access to our repository, you're welcome to have a look at this PR. Copy-paste the code below anywhere in the project to see the error.

The versions of types used:

  • @types/history@4.7.3
  • @types/react@16.9.11
  • @types/react-router-dom@5.1.0
  • @types/recompose@0.30.7

Note: Interestingly enough, if you change:

- declare const Button: React.FunctionComponent<Omit<Props, never>>;
+ declare const Button: React.FunctionComponent<Props>;

it works again despite the fact Omit<Props, never> should be the same as just Props.

Source code
import { History } from 'history'; // "4.7.3"
import * as React from 'react'; // "16.9.11"
import { LinkProps, RouteComponentProps, withRouter } from 'react-router-dom'; // "5.1.0"
import { getDisplayName } from 'recompose'; // "0.30.7"

declare function isDefined<T>(candidate: T | null | undefined): candidate is T;
declare function isString(value?: any): value is string;

type ObjectOmit<T extends K, K> = Omit<T, keyof K>;

type OnClick = NonNullable<React.ComponentProps<'button'>['onClick']>;

type OnClickProp = {
  /** If there is a custom click handler, we must preserve it. */
  onClick?: OnClick;
};

type ProvidedProps = OnClickProp;

type InputProps = OnClickProp & {
  /** Note: we want this helper to work with all sorts of modals, not just those backed by query
   * parameters (e.g. `/photo/:id/info`), which is why this must accept a full location instead of a
   * `Modal` type.
   * */
  to: Exclude<LinkProps['to'], Function>;
};

const buildClickHandler = ({
  to,
  onClick,
  history,
}: InputProps & {
  history: History;
}): OnClick => {
  const navigate = () => {
    // https://github.com/Microsoft/TypeScript/issues/14107
    isString(to) ? history.push(to) : history.push(to);
  };

  return event => {
    [onClick, navigate].filter(isDefined).forEach(callback => callback(event));
  };
};

/** See the test for an example of usage. */
export const enhance = <ComposedProps extends ProvidedProps>(
  ComposedComponent: React.ComponentType<ComposedProps>,
) => {
  type PassThroughComposedProps = ObjectOmit<ComposedProps, ProvidedProps>;
  type OwnProps = InputProps & RouteComponentProps<never> & PassThroughComposedProps;
  type Props = OwnProps;

  const displayName = `CreateModalLink(${getDisplayName(ComposedComponent)})`;

  const ModalLink: React.FunctionComponent<Props> = ({
    to,
    onClick,

    history,
    // We specify these just to omit them from rest props below
    location,
    match,
    staticContext,

    ...passThroughComposedProps
  }) => {
    const clickHandler = buildClickHandler({ to, onClick, history });

    const composedProps: ComposedProps = {
      // Note: this is technically unsafe, since the composed component may have props
      // with names matching the ones we're omitting.
      // https://github.com/microsoft/TypeScript/issues/28884#issuecomment-503540848
      ...((passThroughComposedProps as unknown) as PassThroughComposedProps),
      onClick: clickHandler,
    } as ComposedProps;

    return <ComposedComponent {...composedProps} />;
  };

  ModalLink.displayName = displayName;

  return withRouter(ModalLink);
};

type Props = React.ComponentPropsWithoutRef<'button'> &
  Required<Pick<React.ComponentPropsWithoutRef<'button'>, 'type'>>;

/**
 * This one errors.
 */
declare const Button: React.FunctionComponent<Omit<Props, never>>;

/**
 * This one works.
 */
// declare const Button: React.FunctionComponent<Props>;

const EnhancedButton = enhance(Button);

/**
 * Type instantiation is excessively deep and possibly infinite.ts(2589).
 */
() => <EnhancedButton></EnhancedButton>;

Expected behavior:

I should get a proper error about missing properties (not the one about type instantiation):

Type '{}' is missing the following properties from type 'Readonly<Pick<OwnProps, "form" | "style" | "title" | "onClick" | "to" | "key" | "autoFocus" | "disabled" | "formAction" | "formEncType" | "formMethod" | "formNoValidate" | "formTarget" | ... 252 more ... | "onTransitionEndCapture">>': to, type(2739)

Actual behavior:

I'm getting this:

Type instantiation is excessively deep and possibly infinite.ts(2589).

Playground Link:

Playground Link

Related Issues:

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 6, 2019

| ... 252 more ... |

That's a lot of properties ._.

@karol-majewski
Copy link
Author

karol-majewski commented Nov 6, 2019

@AnyhowStep That's correct. The component in question mixes-in the props defined by JSX.IntrinsicElements['button']. There is a lot going on, and it is complicated, but it's a regression nonetheless because it worked in TypeScript 3.6.3.

@Stianhn
Copy link

Stianhn commented Nov 8, 2019

We have the same issue in one of our larger projects. Works fine with 3.6.4 but not with 3.7.2

Using type keyof JSX.IntrinsicElements

@rista404
Copy link

rista404 commented Nov 8, 2019

Same issue, using keyof JSX.IntrinsicElements type.

@peterkelly
Copy link

peterkelly commented Nov 11, 2019

in src/compiler/checker.ts on line 13208 (in v3.7.2) there is the following code:

function instantiateType(type: Type | undefined, mapper: TypeMapper | undefined): Type | undefined {
    if (!type || !mapper || mapper === identityMapper) {
        return type;
    }
    if (instantiationDepth === 50 || instantiationCount >= 5000000) {
        // We have reached 50 recursive type instantiations and there is a very high likelyhood we're dealing
        // with a combination of infinite generic types that perpetually generate new type identities. We stop
        // the recursion here by yielding the error type.
        error(currentNode, Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
        return errorType;
    }
    instantiationCount++;
    instantiationDepth++;
    const result = instantiateTypeWorker(type, mapper);
    instantiationDepth--;
    return result;
}

So the value is hard-coded. Perhaps a configuration option to change this might be warranted. As a workaround for now, you could try upping this limit and rebuilding the compiler.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 11, 2019

There have been proposals made to increase the limit. They've been rejected because it's seen as an "implementation detail" that should not be exposed.

PR: #29602


Comment by @ahejlsberg

I'm very reluctant to expose the instantiation depth as a configurable parameter as it is an implementation detail that could change. Plus it is next to impossible to explain and reason about what value it should have.

#29511 (comment)

I agree with this part,

it is next to impossible to explain and reason about what value it should have.


If you're writing library code, you should not be messing with this limit. If you're writing application code and can force everyone on your team to use a hacked version of TS...

You can go ahead and hack the .js files for tsc and tsserver. Then, use https://www.npmjs.com/package/patch-package and commit the patch files

@karol-majewski
Copy link
Author

karol-majewski commented Nov 11, 2019

@Stianhn @rista404 Have you managed to reproduce this issue using a more reduced example? If what we're dealing with is a valid use case, then understanding it could help us come up with a heuristic for when type instantiation limit should not be applied.

I agree that messing with the internals leads nowhere — after all, you need to draw the boundary somewhere.

As far as I know, this limit is not something new. It existed before, yet somehow this code stopped working. I wonder what changed that directly influenced our use cases.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 11, 2019

git bisect? =x

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Nov 12, 2019

Can you grant @weswigham permission to the repository as well?

@DanielRosenwasser DanielRosenwasser added Bug A bug in TypeScript Needs Investigation This issue needs a team member to investigate its status. labels Nov 12, 2019
@DanielRosenwasser DanielRosenwasser added this to the TypeScript 3.8.0 milestone Nov 12, 2019
@karol-majewski
Copy link
Author

karol-majewski commented Nov 12, 2019

@DanielRosenwasser Yes! Invite sent @weswigham.

@dgieselaar
Copy link

dgieselaar commented Nov 14, 2019

Another example in https://github.com/elastic/kibana/pull/47188/checks?check_run_id=303150613 where @timroes is trying to upgrade TS to 3.7.2:

x-pack/legacy/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts:58:10 - error TS2589: Type instantiation is excessively deep and possibly infinite.

  58   return fetchAndTransformMetrics({
              ~~~~~~~~~~~~~~~~~~~~~~~~~~
  59     setup,
    ~~~~~~~~~~
... 
  70     additionalFilters: [{ term: { [SERVICE_AGENT_NAME]: 'java' } }]
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  71   });

Related pull request:
elastic/kibana#47188

Related file:
x-pack/legacy/plugins/apm/server/lib/metrics/fetch_and_transform_metrics.ts

edit: this issue will be fixed with the aforementioned pull request. We've added an explicit intermediary type (elastic/kibana@ef912fc) that resolves the issue for 3.7.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 14, 2019

Worth noting that if you're upgrading from TS 3.5, something about how constraintDepth works was changed that can cause the max depth error, for code that was previously working fine (and nesting 100+ layers deep).

#33541


I'm bringing this up because it looks like @dgieselaar is migrating from TS 3.5


I also have a todo on my personal project to investigate this, AnyhowStep/tsql#25

But I'm not focusing on TS 3.7 support yet

@fabb
Copy link

fabb commented Dec 4, 2019

We have a similar issue with this code (roughly):

export type IconProps = TestProps & SizeProps & SpaceProps & DisplayProps & ColorProps<any>

// these are more than 450 props
export type ReactSvgProps = Omit<SVGProps<SVGSVGElement>, 'height' | 'width'>

export const createSvgIcon = (Component: FunctionComponent<ReactSvgProps>) => styled(Component)<IconProps>({display: 'inline-block', flex: '0 0 auto' }, compose(size, space, display, color))

// here we get the error
const MyIcon = createElement(Icon, { color: 'blue', marginRight: 'xs', marginLeft: '-xs'  })

@macmillan78
Copy link

macmillan78 commented Dec 16, 2019

I have a similar issue with grommet-icons v4.4.0:

If I use the trash icon directly, all fine. If I wrap it with styled from styled component, I get the same message. No problem until typescript 3.5.2.

No Problem:

import { FormTrash } from 'grommet-icons';
export const AdProgramView = (props: PropType) => {
    return (
        <Container>
            <h3>
               {props.headline}
                <FormTrash onClick={deleteIntentCallback} />
            </h3>
        </Container>
    )
}

Problem with 3.7.2:

import { FormTrash } from 'grommet-icons';
const FormTrashStyled = styled(FormTrash)`
  vertical-align: bottom;
  cursor: pointer;
`
export const AdProgramView = (props: PropType) => {
    return (
        <Container>
            <h3>
               {props.headline}
                <FormTrashStyled onClick={deleteIntentCallback} />
            </h3>
        </Container>
    )
}

@aeon0
Copy link

aeon0 commented Jan 4, 2020

Same issue with styled components extending rmwc components:

import * as React from 'react'
import styled from 'styled-components'
import { Button } from '@rmwc/button'

const ButtonStyled = styled(Button)`
`

export function ConnectionSetting() {
  return <React.Fragment>
    <ButtonS />
  </React.Fragment>
}

@sanketjo96
Copy link

sanketjo96 commented Jan 28, 2020

So what is the fix here. Nothing yet ? and we should not update to latest ts ? or refactor application code within limits ?

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Jul 13, 2021
@jjangga0214
Copy link
Contributor

jjangga0214 commented Jul 13, 2021

Guys, please show your interest in #44997 (thumb up!) to resolve the issue faster

Currently, the limitation thresholds are hard-coded, to 50 and 5000000.

src/compiler/checker.ts

if (
  instantiationDepth === 50 ||
  instantiationCount >= 5000000
) {
  // We have reached 50 recursive type instantiations and there is a very high likelyhood we're dealing
  // with a combination of infinite generic types that perpetually generate new type identities. We stop
  // the recursion here by yielding the error type.
  error(
    currentNode,
    Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite
  );
  return errorType;
}

To make this configurable, I pushed a PR #44997, which makes those values be able to be set by compilerOptions that can be passed through CLI options or tsconfig.json, like an example below.

{
  "compilerOptions": {
    "instantiationDepthLimit": 100,
    "instantiationCountLimit": 10000000
  }
}

Though the PR is incomplete, just showing your interest would make its priority higher.

Thanks.

@kiprasmel
Copy link

kiprasmel commented Jul 30, 2021

in the meantime, one can use patch-package to create a per-project patch to implement the temporary fix:

  1. edit node_modules/typescript/lib/tsc.js: search for 5000000 or instantiationCount and increate both the instantiationDepth and instantiationCount limits

  2. run node_modules/.bin/patch-package typescript (via yarn patch-package typescript or npx patch-package typescript to generate the patch

should end up something like this in the patches/ folder:

diff --git a/node_modules/typescript/lib/tsc.js b/node_modules/typescript/lib/tsc.js
index 78c5ec2..7d6c586 100644
--- a/node_modules/typescript/lib/tsc.js
+++ b/node_modules/typescript/lib/tsc.js
@@ -49264,7 +49265,7 @@ var ts;
             if (!couldContainTypeVariables(type)) {
                 return type;
             }
-            if (instantiationDepth === 50 || instantiationCount >= 5000000) {
+            if (instantiationDepth === (50 * 2) || instantiationCount >= (5000000 * 2)) {
                 ts.tracing === null || ts.tracing === void 0 ? void 0 : ts.tracing.instant("checkTypes", "instantiateType_DepthLimit", { typeId: type.id, instantiationDepth: instantiationDepth, instantiationCount: instantiationCount });
                 error(currentNode, ts.Diagnostics.Type_instantiation_is_excessively_deep_and_possibly_infinite);
                 return errorType;
  1. make sure to always apply the patch (e.g. after installation) by following patch-package's docs for this purpose (also note this).

  2. if using typescript project references, beware of hoisting of typescript itself - it didn't work for me and resolved to some random package's typescript install - do this in the root package.json (the nohoist):

{
	"name": "...",
	"workspaces": {
		"packages": [
			"...",
			"..."
		],
		"nohoist": [
			"typescript",
			"typescript/**"
		]
	},

Note: after some testing, it seems there's no value that's enough at least for my use case, even though it is not infinitely recursive...

P.S. If a project is using something like babel-loader through webpack (e.g. CRA), I do not know how to configure the tsc executable there or whatever other changes would be needed.

If someone finds this - please let me know.

@koteisaev
Copy link

koteisaev commented Oct 12, 2021

I getting this error in an absurd way in server-side express based app:

    app.get('/api/cms/:module/listing/:category', detectContentModule, loadContentPublicListingPage);

works, but

    app.get('/api/cms/:module/listing/:categoryUrlName', detectContentModule, loadContentPublicListingPage);

ends up with firing this TS2589: Type instantiation is excessively deep and possibly infinite. error.
The rest of code is the same.

Note:
TS 4.3.5 suffers this. Migrated to 4.4.3 with no effect.
Project is mix of TS and JS (transition to TS is in progress).

ts config is following:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "module": "commonjs",
    "target": "ES2018",
    "lib": ["es2018"],
    "allowJs": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "noEmitOnError": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true
  },
  "exclude": ["node_modules", "./dist"],
  "include": ["./src/**/*"]
}

@nawlbergs
Copy link

nawlbergs commented Oct 12, 2021

@koteisaev See my comments above. Its an issue with the @type/express-serve-static-core

@akutruff
Copy link

akutruff commented Oct 12, 2021

TypeScript team - is there any thought of putting in some syntax that could help guide the compiler during inference beyond a heuristic? For libraries that do schema translation this is going to be a continual headache. Perhaps something like a resolve keyword to tell the compiler to fully compute a type resolution, cache it and from then on treat the type as if it were declared by the user explicitly? Is the compiler already doing this as it's resolving types?

// This is a fully resolved type that the compiler knows the user really really wants to happen so go nuts and cache it
type ResolvedType = resolve DeeplyRecursiveType<string, Person>

//This also gives a caching point for each type it is given;
type MySchemaMapper<T> = resolve DeeplyRecursiveType<string, T> 

//Using the above case this:
type MappedPerson = MySchemaMapper<string, Person> 

//is equivalent to this:
type MappedPerson = resolve DeeplyRecursiveType<string, Person> 

Intellisense could also benefit from this as a lot of mapped types end up being unreadable as they show complicated expansions. If the resolve keyword were used, intellisense could display the simplest form of the type declaration and not continue expanding when it encounters a "resolved type." From my experience these deeply recursive types coincide with very generic API boundaries 90% of the time, so it's a natural fit.

@andrewbranch
Copy link
Member

andrewbranch commented Oct 12, 2021

All type instantiations are already cached.

@koteisaev
Copy link

koteisaev commented Oct 12, 2021

@koteisaev See my comments above. Its an issue with the @type/express-serve-static-core

I had to change parameter name to a different yet similar for that URL, as a hotfix.
Lets hope fix you mentioned will be enrolled soon to NPM,

@Ricardo-Marques
Copy link

Ricardo-Marques commented Mar 29, 2022

The depth fix mentioned in this article resolved my problem, even when sticking to an initial depth of 1
https://www.angularfix.com/2022/01/why-am-i-getting-instantiation-is.html

I would love to know why 😄

@ramblehead
Copy link

ramblehead commented Nov 4, 2022

It is possible to avoid Subj. issue in certain use cases by utilising UnionToIntersection metafunction as shown in the following SO answer:

https://stackoverflow.com/a/74292294/5000057

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 Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

Successfully merging a pull request may close this issue.