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 · 72 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:

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

| ... 252 more ... |

That's a lot of properties ._.

@karol-majewski
Copy link
Author

@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

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

@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

git bisect? =x

@DanielRosenwasser
Copy link
Member

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

@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

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

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

@OnkelTem
Copy link

OnkelTem commented Aug 28, 2023

Was also experimenting with Zod schemas. In my case, this would negate all my efforts to validate the API using Zod.

Here is a sandbox, where I try to make a Zod union of basic object keys.

In addition to showing the error message, an unexpected value of unknown starts to appear at the beginning of the tuple. I have no idea where it comes from tho.

image

@andelkocvjetkovic
Copy link

andelkocvjetkovic commented Oct 20, 2023

I had similar issue with zod, instead using z.merge or z.extend I used spread operators

export const addNewProspectSchema = z.object({
  ...generalSchema.shape,
  ...landSchema.shape,
  ...pvResourceSchema.shape,
  ...windResourceSchema.shape,
  ...productionSchema.shape,
  ...gridSchema.shape,
  ...
});

type AddNewProspectSchema = z.infer<typeof addNewProspectSchema>;

This solved the issue, at least for now. :)

@ryan-williams
Copy link

In case it helps someone, I think I was triggering this error attempting to combine some Promise<JSX.Element>s with Promise<ReactNode>s in one array:

const elems: Promise<JSX.Element>[] = []
const nodes: Promise<ReactNode>[] = []
const all = Promise.all([ ...elems, ...nodes ])

This fails to compile:

Type error: Type instantiation is excessively deep and possibly infinite.

  112 |     const elems: Promise<JSX.Element>[] = []
  113 |     const nodes: Promise<ReactNode>[] = []
> 114 |     const all = Promise.all([ ...elems, ...nodes ])
      |                 ^

In my case, both arrays could be refined to Promise<JSX.Element>s, and that fixed the error. but in general the other direction ("widening" JSX.Elements to ReactNodes) should always work.

@Amorim33
Copy link

Amorim33 commented Oct 31, 2023

I had similar issue with zod, instead using z.merge or z.extend I used spread operators

export const addNewProspectSchema = z.object({
  ...generalSchema.shape,
  ...landSchema.shape,
  ...pvResourceSchema.shape,
  ...windResourceSchema.shape,
  ...productionSchema.shape,
  ...gridSchema.shape,
  ...
});

type AddNewProspectSchema = z.infer<typeof addNewProspectSchema>;

This solved the issue, at least for now. :)

Very nice!!

I have a similar problem.

I was using merge, pick and extend. After seeing your comment, I simplified the schema using the spread.
However, the error continues to appear, probably the cause is now the pick method.

Any ideas on how to resolve it?

const getMonthlyInsuredItemSchema = monthlyInsuredItemSchema.pick({
  month: true,
  processedAt: true,
});

const getInsuredItemSchema = insuredItemSchema.pick({
  externalId: true,
  additionalAttributes: true,
  remainingInstallments: true,
  totalInstallments: true,
  installmentAmount: true,
});

const getInsuredPersonSchema = insuredPersonSchema.pick({
  name: true,
  cpf: true,
  birthdate: true,
  gender: true,
});

const getInsuredItemsResponse = z.array(
  z.object({
    ...getMonthlyInsuredItemSchema.shape,
    ...getInsuredItemSchema.shape,
    insuredPerson: getInsuredPersonSchema,
  }),
);

Update

The problem was in additionalAttributes .

I believe that what causes the error is the fact that the field is a jsonb in the database, receives a type transformation from drizzle-zod, and is then extended in another schema, generating many recurring type instantiations.

What I did was remove the additionalAttributes from the pick and redefine it explicitly in the getInsuredItemsResponse schema.

banderror pushed a commit to elastic/kibana that referenced this issue Nov 29, 2023
…improve validation error messages (#171452)

**Epics:** elastic/security-team#8058,
elastic/security-team#6726 (internal)
**Partially addresses:**
elastic/security-team#7991 (internal)

## Summary

The main benefit of this PR is shown in `rule_request_schema.test.ts`,
where the error messages are now more accurate and concise. With regular
unions, `zod` has to try validating the input against all schemas in the
union and reports the errors from every schema in the union. Switching
to discriminated unions, with `type` as the discriminator, allows `zod`
to pick the right rule type schema from the union and only validate
against that rule type. This means the error message reports that either
the discriminator is invalid, in any case where `type` is not valid, or
if `type` is valid but another field is wrong for that type of rule then
the error message is the validation result from only that rule type.

To make it possible to use discriminated unions, we need to switch from
using zod's `.and()` for intersections to `.merge()` because `.and()`
returns an intersection type that is incompatible with discriminated
unions in zod. Similarly, we need to remove the usage of `.transform()`
because it returns a ZodEffect that is incompatible with `.merge()`.

Instead of using `.transform()` to turn properties from optional to
possibly undefined, we can use `requiredOptional` explicitly in specific
places to convert the types. Similarly, the `RequiredOptional` type can
be used on the return type of conversion functions between API and
internal schemas to enforce that all properties are explicitly specified
in the conversion.

Future work:
- better alignment of codegen with OpenAPI definitions of anyOf/oneOf.
https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#oneof
oneOf requires that the input match exactly one schema from the list,
which is different from z.union. anyOf should be z.union, oneOf should
be z.discriminatedUnion
- flatten the schema structure further to avoid `Type instantiation is
excessively deep and possibly infinite`. Seems to be a common issue with
zod (microsoft/TypeScript#34933) Limiting the
number of `.merge` and other zod operations needed to build a particular
schema object seems to help resolve the error. Combining
`ResponseRequiredFields` and `ResponseOptionalFields` into a single
object rather than merging them solved the immediate problem. However,
we may still be near the depth limit. Changing `RuleResponse` as seen
below also solved the problem in testing, and may give us more headroom
for future changes if we apply techniques like this here and in other
places. The difference here is that `SharedResponseProps` is only
intersected with the type specific schemas after they're combined in a
discriminated union, whereas in `main` we merge `SharedResponseProps`
with each individual schema then merge them all together.
- combine other Required and Optional schemas, like
QueryRuleRequiredFields and QueryRuleOptionalFields

```ts
export type RuleResponse = z.infer<typeof RuleResponse>;
export const RuleResponse = SharedResponseProps.and(z.discriminatedUnion('type', [
  EqlRuleResponseFields,
  QueryRuleResponseFields,
  SavedQueryRuleResponseFields,
  ThresholdRuleResponseFields,
  ThreatMatchRuleResponseFields,
  MachineLearningRuleResponseFields,
  NewTermsRuleResponseFields,
  EsqlRuleResponseFields,
]));
```

---------

Co-authored-by: Kibana Machine <[email protected]>
@natew
Copy link

natew commented Jan 24, 2024

One point in favor of upping the limit besides the weeks of my life wasted that I've spent battling these problems, is that I actually could easily speed up the tamagui types by 2x, except that it starts hitting complexity limits.

So in fact the complexity limit is making things slower in practice rather than faster.

@aleczratiu
Copy link

Hitting this issue. Using zod to merge several sub-schemas into a "complete" schema, resulting in the error. Hard-coding the resultant large schema works fine, but is more of a pain to maintain/less re-usable

The same issue I have using zod, did you find any workarounds for it?

@Charismara
Copy link

Hitting this issue. Using zod to merge several sub-schemas into a "complete" schema, resulting in the error. Hard-coding the resultant large schema works fine, but is more of a pain to maintain/less re-usable

The same issue I have using zod, did you find any workarounds for it?

I got the same issue. Is there a workaround for this?

@Threebow
Copy link

Threebow commented Jun 11, 2024

What I interpret from this issue, and the comments within, is that that once your codebase/typing logic grows enough in size or complexity to an arbitrary point that TypeScript is unhappy with, you are no longer able to compile the project.

So, am I correct to conclude from this, that TypeScript enforces an upper limit on the size of project that you are able to develop with it, and any larger/more complex projects are entirely unsupported by the compiler?

Somebody please correct me if I am wrong, but the fact that you can spend years working on a project and one day hit an arbitrary "complexity limit" seems grossly incorrect and inappropriate for a tool of this breadth in the industry.

If this is indeed the case, and I am not mistaken, then:

  1. What is the accepted "fix" to spending years on a project just to ultimately hit this limit and be faced with a compiler that outright refuses to compile the project? Are we doomed to throw the project out and rewrite the whole thing in a different stack, or put an indefinite pause on development until this issue is fixed?
  2. Where does TypeScript make this clear enough to a potential user looking to adopt TypeScript, to where they will choose a different tool for a project if they know their project will grow to this arbitrary complexity limit, to completely avoid wasting years of their and their teammates' time?

Open to input and corrections from others who have bumped into this.

@natew
Copy link

natew commented Jun 11, 2024

I think this really needs an option to just increase the size.

The reason this is such a big problem is that - only the biggest projects are affected.

IE, you've spent the most amount of effort investing into TypeScript, to the point where your types are really large. But now it actually comes around and bites you, and often in a way you just can't control. If a library you use everything in your project has complex types and you've used it 2k times across your codebase, and on the 2001 time it suddenly craps out, you're looking at losing type safety on one of your most critical/used pieces of the stack.

The second reason I think we need a way around this is because the warning actually doesn't correlate with performance. I've landed big improvements to performance that caused this warning to occur. So it actually is a safeguard that is working against you oftentimes.

@Threebow
Copy link

Threebow commented Jun 11, 2024

@natew

I think this really needs an option to just increase the size.

TS has stated five years ago in 2019 that they will not expose this option to users because it's an implementation detail of an internal TypeScript mechanism that they may choose to eliminate in the future.

Another person has said the same thing, over a year later, in 2020.

These decisions seem a little short-sighted in hindsight, given that it's been five years, and that they have seemingly forgotten about this mechanism in its entirety.

IE, you've spent the most amount of effort investing into TypeScript, to the point where your types are really large. But now it actually comes around and bites you, and often in a way you just can't control.

I strongly resonate with this. It feels like a kick in the groin to hear that your usage of the system is outside of what they support, after investing so heavily in the ecosystem and the language itself.

If a library you use everything in your project has complex types and you've used it 2k times across your codebase, and on the 2001 time it suddenly craps out, you're looking at losing type safety on one of your most critical/used pieces of the stack.

This issue is only going to start popping up more and more, as more people start to get into codegen of complicated type systems.

@tran-simon
Copy link

I get this issue all the time beacuse I'm using json-schema-to-ts

It's documented in their FAQ, but there doesn't seem to be any fix

@Xriuk
Copy link

Xriuk commented Jun 12, 2024

What I interpret from this issue, and the comments within, is that that once your codebase/typing logic grows enough in size or complexity to an arbitrary point that TypeScript is unhappy with, you are no longer able to compile the project.

More or less, the main problem with increasing type instantiation depth appears to be that some web instances of TypeScript just crash, because TypeScript is meant to be running everywhere apparently.
This is frustrating because at least for me TypeScript just compiles code into JavaScript, so no matter the complexity and depth of types, the resulting code should always be the same. That's why we resorted to using a custom TypeScript version with these values increased freely.

@Threebow
Copy link

some web instances of TypeScript just crash, because TypeScript is meant to be running everywhere apparently

Now I am curious if there is a limit to where TypeScript crashes simply because the compilation time exceeds an arbitrary "too long to compile" point. It seems strange that a long compilation time would force a crash, as opposed to a simple freeze.

That seems like a separate issue in its entirety, unrelated to this one. I'm also not really sure who's compiling a project with 2000+ Zod usages in their browser at runtime...

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.