类型实例化过深,可能无限。

huangapple go评论87阅读模式
英文:

Type instantiation is excessively deep and possibly infinite

问题

如何处理这个错误?我可以限制递归深度,还是只是告诉 TypeScript 这是可以的?

export type StateUnion<T> = T extends { states: Record<infer K, any> }
  ? K | StateUnion<T['states'][K]>
  : never;

Playground


<details>
<summary>英文:</summary>

How to handle this error here? Can I limit the recursion depth, or just tell TS that it&#39;s ok?

export type StateUnion<T> = T extends { states: Record<infer K, any> }
? K | StateUnion<T['states'][K]>
: never;


[Playground][1]


  [1]: https://www.typescriptlang.org/play?ts=4.4.0-beta#code/KYDwDg9gTgLgBDAnmYcDKMCGNgFUB2AlhPgDwAqAfHALxzlyg74AmAznAN5xtY5sAuOACVgAY2gtShfADNgUOAGkANHEz5E1AL4AoOHAD8yuAB90fPERIUA2gHJe2YG3sBdW0reV9cIfmAANwUAbl1w0EhYOBkcKFlMMVQAYRJZQgBzLl8nfkMhUQkoKV4oGQy1VLlMyjC9XUjoeFkAV3wxGGJ8OABbRIALGWAKRhBmdjgq9IzKAAoJaoyhcgBKbIMoYBgWqG7OXwMYKA02Qk6SFV9tMN9W9vPuo5Ozrtnc4CEMZwIuiko1-YGAwLXg8SwYRR0d5hAx6PRAA

</details>


# 答案1
**得分**: 0

以下是您要翻译的内容:

"When you have a [recursive conditional type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types) like `StateUnion` and apply it to a type that is itself recursive, like `Config`, you can easily get into situations where the compiler complains about recursion depth. Sometimes you can tweak one or both of your definitions to sidestep the problem, but those approaches tend to be trial-and-error (at least in my experience).

有一个[递归条件类型](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#recursive-conditional-types),例如`StateUnion`,并将其应用于自身递归的类型,例如`Config`,您很容易陷入编译器关于递归深度的投诉。有时您可以调整一个或两个定义,以规避问题,但这些方法往往是试错的(至少在我的经验中是如此)。

One approach you might try is to give a explicit depth limiting mechanism to your recursive conditional type, picking a reasonable default maximum depth but allowing users to change it. For your `StateUnion` it could look like this:

您可以尝试的一种方法是为递归条件类型提供一个明确的深度限制机制,选择一个合理的默认最大深度,但允许用户更改它。对于您的`StateUnion`,它可能如下所示:

```typescript
type StateUnion<T, D extends number = 8, A extends any[] = []> =
  A['length'] extends D ? never : (
    T extends { states: Record<infer K, any> }
    ? K | StateUnion<T['states'][K], D, [0, ...A]>
    : never
  );

Here the intended use is StateUnion<T, D> where D is a numeric literal type corresponding to the desired max depth. If you write StateUnion<T> without specifying D then the max depth is 8 (but you can change that).

在这里,预期的用法是StateUnion<T, D>,其中D是与所需的最大深度对应的数值字面类型。如果不指定D而编写StateUnion<T>,则最大深度为8(但您可以更改它)。

Unfortunately TypeScript (as of v5.1) doesn't know how to do math at the type level directly, so I can't easily say something like StateUnion<T['states'][K], D-1>. Instead I resort to using tuple types which have a known numeric literal length property, and variadic tuple types to manipulate these types. So instead of the normal "if depth is 0 then bail out, otherwise reduce depth by 1 and recurse", I have to say something like "if the length of this array is the same as the maximum depth then bail out, otherwise append an element to the array and recurse". So I have that accumulator array A in there, and we are checking A["length"] and passing in [0, ...A] as the new A.

不幸的是,TypeScript(截至版本5.1)不知道如何在类型级别直接进行数学运算,因此我无法轻松地说类似StateUnion<T['states'][K], D-1>的内容。相反,我使用元组类型,它们具有已知的数值字面量length属性,以及可变元组类型来操作这些类型。因此,与正常的“如果深度为0,则退出,否则减小深度1并递归”不同,我必须说类似“如果此数组的长度与最大深度相同,则退出,否则向数组附加一个元素并递归”。因此,我在其中有一个累加器数组A,并检查A["length"],并将[0, ...A]传递为新的A

Let's test it:

让我们来测试一下:

interface Test {
  states: { a: { states: { b: { states: { c: {
    states: { d: { states: { e: Test } } } 
  } } } } } }
}

type SU2 = StateUnion<Test, 2>
//type SU2 = "a" | "b"
type SU4 = StateUnion<Test, 4>
//type SU4 = "a" | "b" | "c" | "d"
type SU8 = StateUnion<Test> // defaults to 8
// type SU8 = "a" | "b" | "c" | "d" | "e"

That works as expected, and if I make that change then your original code with StateUnion<Config> (or StateUnion<T> where T extends Config) compiles without error.

这按预期工作,如果我进行这些更改,那么您的原始代码中的StateUnion<Config>(或T extends ConfigStateUnion<T>)将编译而无误。

[Playground link to code](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcDKMCGNgFUB2AlhPgDwAqANHACJyg74AmAznPgK4C2ARsFHAC8cABzUAgvRCNWcTPkQBtALpC4KgHxCAUHDjjFAcgA2wfAHMYAC0OqGZ2XQD87YADd+cAFxwAFLr04cikZNgBvOBYsHBYfACVgAGNoJlJCfAAzTwBpanlELQBfAL0XbLgAH3RovCISCiMo7GAWW0Vs5W

英文:

When you have a recursive conditional type like StateUnion and apply it to a type that is itself recursive, like Config, you can easily get into situations where the compiler complains about recursion depth. Sometimes you can tweak one or both of your definitions to sidestep the problem, but those approaches tend to be trial-and-error (at least in my experience).

One approach you might try is to give a explicit depth limiting mechanism to your recursive conditional type, picking a reasonable default maximum depth but allowing users to change it. For your StateUnion it could look like this:

type StateUnion&lt;T, D extends number = 8, A extends any[] = []&gt; =
  A[&#39;length&#39;] extends D ? never : (
    T extends { states: Record&lt;infer K, any&gt; }
    ? K | StateUnion&lt;T[&#39;states&#39;][K], D, [0, ...A]&gt;
    : never);

Here the intended use is StateUnion&lt;T, D&gt; where D is a numeric literal type corresponding to the desired max depth. If you write StateUnion&lt;T&gt; without specifying D then the max depth is 8 (but you can change that).

Unfortunately TypeScript (as of v5.1) doesn't know how to do math at the type level directly, so I can't easily say something like StateUnion&lt;T[&#39;states&#39;][K], D-1&gt;. Instead I resort to using tuple types which have a known numeric literal length property, and variadic tuple types to manipulate these types. So instead of the normal "if depth is 0 then bail out, otherwise reduce depth by 1 and recurse", I have to say something like "if the length of this array is the same as the maximum depth then bail out, otherwise append an element to the array and recurse". So I have that accumulator array A in there, and we are checking A[&quot;length&quot;] and passing in [0, ...A] as the new A.

Let's test it:

interface Test {
  states: { a: { states: { b: { states: { c: {
    states: { d: { states: { e: Test } } } 
  } } } } } }
}

type SU2 = StateUnion&lt;Test, 2&gt;
//type SU2 = &quot;a&quot; | &quot;b&quot;
type SU4 = StateUnion&lt;Test, 4&gt;
//type SU4 = &quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot;
type SU8 = StateUnion&lt;Test&gt; // defaults to 8
// type SU8 = &quot;a&quot; | &quot;b&quot; | &quot;c&quot; | &quot;d&quot; | &quot;e&quot;

That works as expected, and if I make that change then your original code with StateUnion&lt;Config&gt; (or StateUnion&lt;T&gt; where T extends Config) compiles without error.

Playground link to code

huangapple
  • 本文由 发表于 2023年6月5日 19:04:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/76405795.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定