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 with PartialRecursive & recursive list type #35156

Closed
idoros opened this issue Nov 17, 2019 · 5 comments
Closed
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@idoros
Copy link

idoros commented Nov 17, 2019

TypeScript Version: 3.7.2 (and nightly/next)

Search Terms:

  • Type instantiation is excessively deep and possibly infinite

Code

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json } // recursive  object doesn't cause any issues
    | Json[] // removing the list option resolves the issue

type PartialRecursive<T> = T extends object
    ? { [K in keyof T]?: PartialRecursive<T[K]> }
    : T

interface Data {
    value: Json
}

function acceptData(param: PartialRecursive<Data>) {}

const data = {} as Data
acceptData(data) // error TS2589: Type instantiation is excessively deep and possibly infinite.

Expected behavior:
Value field should be valid as both the type itself is already inferred as the Data type and the entire type is optional

Actual behavior:
The recursive list type causes an infinite (or excessive) calculation that fails.

Playground Link:

Related Issues:

@jcalz
Copy link
Contributor

jcalz commented Nov 17, 2019

Related: #33050, which introduced the ability to write type Json = ... | Json[] instead of requiring an intervening interface like type Json = ... | JsonArray; interface JsonArray extends Array<Json> {}.

@idoros
Copy link
Author

idoros commented Nov 17, 2019

Looking at this some more I noticed that the problem is in my own (probably copied from somewhere) PartialRecursive type that doesn't handle Array types.

This fixes my case:

type PartialRecursive<T> = T extends object
    ? T extends Array<infer U> ? PartialRecursive<U>[]
    : { [K in keyof T]?: PartialRecursive<T[K]> }
    : T

@idoros idoros closed this as completed Nov 17, 2019
@jcalz
Copy link
Contributor

jcalz commented Nov 17, 2019

I wouldn't be so quick to dismiss this; since TypeScript 3.1, mapped array/tuple types have been supported so you shouldn't have to special-case arrays like that. Indeed, the pre-TS-3.7 way of typing Json does not have this problem with your original PartialRecursive:

type Json =
    | string
    | number
    | boolean
    | null
    | { [property: string]: Json }
    | JsonArray

interface JsonArray extends Array<Json> {}

type PartialRecursive<T> = T extends object
    ? { [K in keyof T]?: PartialRecursive<T[K]> }
    : T

interface Data {
    value: Json
}

function acceptData(param: PartialRecursive<Data>) {}

const data = {} as Data
acceptData(data) // no error

Playground link

So whatever's going on here isn't what I'd call a problem with recursive mapped types. I don't know if this is a compiler bug / design limitation / working as intended, but I'm still interested in seeing what an official word here would be.

@idoros
Copy link
Author

idoros commented Nov 17, 2019

Re-opening, feel free to close if this works as expected

@idoros idoros reopened this Nov 17, 2019
@andrewbranch andrewbranch added the Needs Investigation This issue needs a team member to investigate its status. label Dec 17, 2019
@ahejlsberg ahejlsberg added Design Limitation Constraints of the existing architecture prevent this from being fixed and removed Needs Investigation This issue needs a team member to investigate its status. labels Jul 14, 2021
@ahejlsberg
Copy link
Member

This is effectively a design limitation.

When a homomorphic mapped type is applied to an array type, we obtain the element type of the array and apply the mapped type's template type to that. This is done eagerly, which effectively causes a circularity when the type being mapped is itself circular.

As mentioned above, the original example works when an array case is added to the conditional type:

type PartialRecursive<T> = 
    T extends (infer U)[] ? PartialRecursive<U>[] :
    T extends object ? { [K in keyof T]?: PartialRecursive<T[K]> } :
    T;

Here, PartialRecursive<U>[] is represented internally as a deferred type reference, and this saves us from the eager circularity problem. We could consider doing something similar for the case of applying a mapped type to an array type, but I don't think it is super important given that we have a very reasonable workaround.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants