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

Version 3.4-dev breaks recursive types #30188

Closed
millsp opened this issue Mar 2, 2019 · 26 comments
Closed

Version 3.4-dev breaks recursive types #30188

millsp opened this issue Mar 2, 2019 · 26 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@millsp
Copy link
Contributor

millsp commented Mar 2, 2019

TypeScript Version: 3.4.0-dev.20190302

Search Terms: recursive types

screenshot from 2019-03-04 16-09-21

I recently published an article about how to create types for curry and ramda. Quite a few people are excited and waiting for me to add these types to DefinitelyTyped. But I can't pass the lint tests yet.

To create these (curry) types, I detail how I make use of recursive types. But TS 3.4.0-dev.20190302 is indeed breaking these types. In the latest stable version (3.3.3333) warnings arise (only) when we recurse more than 45 times. A recursive type then returns any if the limit is exceeded (which is nice).

But in TS 3.4.0-dev.20190302 it appears that "Type instantiation is excessively deep and possibly infinite", which is a breaking behaviour. But in fact it is only possibly infinite, and this is why the previous behaviour should be preferred. any could just be returned anytime that limit has been exceeded, thus stopping the recursion condition.

However, recursive types seem to break only when they are nested. Any idea why @ahejlsberg ?
By breaking this feature, we cannot expect complex types for curry and other tools from ramda.

Code
Article: https://medium.freecodecamp.org/typescript-curry-ramda-types-f747e99744ab
Repo : https://github.com/pirix-gh/medium/blob/master/types-curry-ramda/src/index.ts

Expected behavior:
Updates should not have breaking behaviors

Actual behavior:
Breaking behavior on recursive types

Playground Link:

Related Issues:
#29511

@millsp millsp changed the title TS 3.4-dev breaks recursive types Version 3.4-dev breaks recursive types Mar 2, 2019
@ahejlsberg
Copy link
Member

@pirix-gh When we hit the instantiation depth limiter we force the type to terminate by resolving to an any. The only change in 3.4 is that we now report an error to let you know that we are delivering truncated (and unpredictable) results. Previously we'd just silently let it pass. So, I'm quite certain you were triggering the limiter before, it's just that you weren't hearing about it.

@ahejlsberg
Copy link
Member

I should add that there are two places where we may report that error. One is an instantiation depth of 50 (indicating a possibly infinite type), the other is a constraint depth of 50 (indicating a possibly infinite constraint).

@millsp
Copy link
Contributor Author

millsp commented Mar 4, 2019

Thanks @ahejlsberg for your quick reply. Understood.

But from another point of view. Since this is the only way (for now) to build (analysis) types for curry, and a lot of Ramda users are expecting them, can I still publish to DefinitelyTyped even if it does not pass the lint tests (exceptionally)?

How could we work this out @sandersn ?

@KSXGitHub
Copy link
Contributor

How do I ignore this particular type of errors?

@millsp
Copy link
Contributor Author

millsp commented Apr 2, 2019

I you run this on TS 3.4, you should see an error:

type Test00<T1 extends any[], T2 extends any[]> =
    Reverse<Cast<Reverse<T1>, any[]>, T2>

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

It happens when TS decides that types become too complex to compute (ie).
The solution is to compute the types that cause problems step by step:

type Test01<T1 extends any[], T2 extends any[]> =
    Reverse<Reverse<T1> extends infer R ? Cast<R, any[]> : never, T2>

@remojansen
Copy link
Contributor

remojansen commented Apr 9, 2019

Can this error be disabled? I was unable to find it in the 3.4 breaking changes https://devblogs.microsoft.com/typescript/announcing-typescript-3-4/#breaking-changes

Update

This issue causes a lot of errors in the code base that we have at work. It is a large mono repo and we have a lot of recursive types. From example, we have types that map objects into immutable objects and we also have types to work with some data structures such as trees and graphs which are also recursive.

@typeofweb
Copy link

This is a breaking change, my project won't compile :|
Should this be reverted in 3.x and rereleased as 4.0?

@jmrog
Copy link

jmrog commented Apr 12, 2019

@pirix-gh When we hit the instantiation depth limiter we force the type to terminate by resolving to an any. The only change in 3.4 is that we now report an error to let you know that we are delivering truncated (and unpredictable) results. Previously we'd just silently let it pass. So, I'm quite certain you were triggering the limiter before, it's just that you weren't hearing about it.

It's entirely possible that I'm misunderstanding the above, but I can say with at least some confidence that, with TypeScript 3.3.3, many of my recursive types resolved to types that were very informative/helpful (not just to any), whereas, with TypeScript 3.4+, those exact same types now fail to resolve at all, and the "excessively deep and possibly infinite" error is reported. So, there appears to be more going on here than just error reporting.

Because of the above, I'd also appreciate this being regarded as a breaking change.

@RyanCavanaugh
Copy link
Member

We don't bump the major version for breaking changes (but thank you for thinking we've only ever had two breaking changes in six years of development! 😉)

@jmrog
Copy link

jmrog commented Apr 12, 2019

We don't bump the major version for breaking changes (but thank you for thinking we've only ever had two breaking changes in six years of development! 😉)

That would be impressive. 😄But (if this was a reply to my latest comment) my hope was just that it would be regarded as a breaking change however it is that you regard things as breaking changes -- e.g., by documenting it here.

@seems112
Copy link

seems112 commented Apr 18, 2019

I got the same error

export type SendFuncType<T> =
				T extends 1? FuctionType1:
				T extends 2? FuctionType2:
				T extends 3? FuctionType3:
				T extends 4? FuctionType4:
				T extends 5? FuctionType5:
				T extends 6? FuctionType6:
				T extends 7? FuctionType7:
				T extends 8? FuctionType8:
				T extends 9? FuctionType19:
				T extends 10? FuctionType10:
				T extends 11? FuctionType11:
				T extends 12? FuctionType12:
                                ...
                                T extends 73? FuctionType73:
                                undefine

The question mark expression about 73 lines, and it writen by tools.
vscode give me the error:
Type instantiation is excessively deep and possibly infinite.ts(2589)

@artalar
Copy link

artalar commented May 28, 2019

Maybe add posibility to customize "depth" of cheking by config option?

@artalar
Copy link

artalar commented May 28, 2019

Check this workaround ksxnodemodules/typescript-tuple#8 (comment) by @KSXGitHub

@KSXGitHub
Copy link
Contributor

Check this workaround ksxnodemodules/typescript-tuple#8 (comment) by @KSXGitHub

The workaround does not solve everything though. It can only be used in generated type (not one that is written by hand) and it is only applicable in certain context (conditions must be convertible to property names).

typeofweb added a commit to typeofweb/typesafe-hapi that referenced this issue Jun 3, 2019
@regevbr
Copy link

regevbr commented Jun 26, 2019

I get the same.

First function works, the 2nd (and on wards) doesn't...

// No error
export function getPropOr<T,
  K1 extends keyof NonNullable<T>,
  K2 extends keyof NonNullable<NonNullable<T>[K1]>,
  K3 extends keyof NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>,
  K4 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>,
  K5 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>>(
  obj: T, defaultValue: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]> | (() => NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>),
  k1: K1, k2: K2, k3: K3, k4: K4, k5: K5):
  NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>;

// Produces the error
export function getPropOr<T,
  K1 extends keyof NonNullable<T>,
  K2 extends keyof NonNullable<NonNullable<T>[K1]>,
  K3 extends keyof NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>,
  K4 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>,
  K5 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>,
  K6 extends keyof NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>>(
  obj: T, defaultValue: NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>[K6]> | (() => NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>[K6]>),
  k1: K1, k2: K2, k3: K3, k4: K4, k5: K5, k6: K6):
  NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<NonNullable<T>[K1]>[K2]>[K3]>[K4]>[K5]>[K6]>;

@millsp
Copy link
Contributor Author

millsp commented Jun 26, 2019

@regevbr are you trying to make an object deeply non nullable ?

@regevbr
Copy link

regevbr commented Jun 26, 2019

@pirix-gh based on https://www.reddit.com/r/typescript/comments/aynx0o/safe_deep_property_access_in_typescript I created the code in
https://gist.github.com/regevbr/57f3b4d798fb4642eb4a1c6ed667320d

What it does, is provide you with a way to perform a safe, and type safe nested property access of an object.

So the following works:

interface Foo {
  a?: {
    b?: {
      c?: {
        d?: {
          e: number;
        };
      };
    };
  };
}
const obj: Foo = {};
console.log(getPropOr(obj, 0, 'a', 'b', 'c', d', 'e').toFixed(0)); // Should print 0

@millsp
Copy link
Contributor Author

millsp commented Jun 26, 2019

@regevbr ts-toolbelt can solve your problem

I just rewrote getProp types based on what you've said.

import {O} from 'ts-toolbelt'

declare function getProp<O extends object, P extends string[]>(o: O, ...path: P): O.Path<O, P>

const o0 = {a: {b: {c: 'c'}}}
const o1 = {a: {b: {c: 100}}}

const t0 = getProp(o0, 'a', 'b', 'c') // string
const t1 = getProp(o1, 'a', 'b', 'c') // number

You can write much shorter types with ts-toolbelt, it computes for you.

@regevbr
Copy link

regevbr commented Jun 26, 2019

@pirix-gh that looks like a really cool library. But your solution doesn't provide all I need.

For example the following returns type never, wheres my code will not even allow you to write it as it won't compile.

import * as tb from 'ts-toolbelt';

declare function getProp<O extends object, P extends string[]>(o: O, ...path: P): tb.O.Path<O, P>;

const o0 = { a: { b: { c: 'c' } } };
const o1 = { a: { b: { c: 100 } } };

const t0 = getProp(o0, 'a', 'b', 'd'); // 'd' is not a valid propery

Also I also have getPropOr which returns a default value (or computes a default value from a given function for lazy evaluation of the default value). Is it possible to achieve it with toolbelt?

@millsp
Copy link
Contributor Author

millsp commented Jun 26, 2019

@regevbr thanks & sorry, I did not think about that one. Here's something more suited:

import {O, A} from 'ts-toolbelt'

declare function getProp<O extends object, P extends string[]>(
    o: O,
    ...path: A.Cast<P, O.PathValid<O, P>>
    // `Cast` adds a constraint `PathValid` to `P`
): O.Path<O, P>

const o0 = {a: {b: {c: 'c'}}, b: {}}
const o1 = {a: {b: {c: 100}}}

const t0 = getProp(o1, 'a', 'b', 'c') // number
const t1 = getProp(o1, 'a', 'b', 'c') // number
const t2 = getProp(o1, 'a', 'b', 'x') // error
const t3 = getProp(o0, 'x', 'b', 'c') // error

Thanks, you gave birth to a brand new utility type!

And, remember to update to the latest release :)

@regevbr
Copy link

regevbr commented Jun 26, 2019

@pirix-gh thanks!

I tried (until you replied) creating that type (PathValid) myself without luck.
Your typescript skills are mental!

Can you help me understand why you add

[TYPE] extends infer X ? Cast<X, any[]> : never;

To the return type of every signature in the toolbelt library?

By the way your suggestion doesn't support they any type for T

import { Object, Any } from 'ts-toolbelt';

export function getProp<O extends object, P extends string[]>(obj: O,
                                                              ...keys: Any.Cast<P, Object.PathValid<O, P>>)
  : Object.Path<O, P> {
  return keys.reduce(
    (result: any, key: string) => (result === null || result === undefined) ? undefined : result[key],
    obj);
}

const o1: any = { a: { b: { c: 100 } } } ;

const t0 = getProp(o1, 'a', 'b', 'c'); //  Argument of type '"a"' is not assignable to parameter of type 'HasPath<any, ["a"], any, "default"> extends true ? "a" : never'.

Can you suggest a fix for that?

@millsp
Copy link
Contributor Author

millsp commented Jun 26, 2019

Yes, I will provide a fix for this. Thanks for pointing it out. What is your TS version?

@regevbr thanks. I use this syntax to defer the evaluation of a type... Otherwise TS evaluates all the types in their full depth. This causes performance issues and is irrelevant because we only want to compute when we've received the type parameters. As a result, the ts-toolbelt types load fast.

But it is not the only use case. Like I said above, TS can complain that a type is too deep to compute, then you can force it to compute step by step. So I especially use this on recursive types (which are much deeper because they're recursive).

And one last reason is that it resets the type nesting count. TS also prevents to nest too many types, and throws errors when we do so. But since one of the goals of this lib is to combine types together, it is wise to reset that depth count (that is increased by the utility type itself).

In some cases it might not be wise to do this, as it can make TS swallow warnings. And this is the reason why ts-toolbelt is thoroughly tested and all the types are benchmarked (by me by hand, for now).

@millsp
Copy link
Contributor Author

millsp commented Jun 26, 2019

Closing issue, as this is clearly a #wontfix. If you need to create complex types, please use ts-toolbelt.

@millsp millsp closed this as completed Jun 26, 2019
@regevbr
Copy link

regevbr commented Jun 27, 2019

@pirix-gh thanks for the info!

@xeptore
Copy link

xeptore commented Sep 30, 2020

I you run this on TS 3.4, you should see an error:

type Test00<T1 extends any[], T2 extends any[]> =
    Reverse<Cast<Reverse<T1>, any[]>, T2>

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

It happens when TS decides that types become too complex to compute (ie).
The solution is to compute the types that cause problems step by step:

type Test01<T1 extends any[], T2 extends any[]> =
    Reverse<Reverse<T1> extends infer R ? Cast<R, any[]> : never, T2>

Hi! Thanks! You saved my day! 🙏

@PriscilaLSouza
Copy link

PriscilaLSouza commented Aug 26, 2022

Eu possuo esse erro aqui, alguém poderia ajudar?... A instanciação de tipo é muito profunda e possivelmente infinita.ts(2589)
Captura de Tela 2022-08-26 às 12 33 19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests