typescript 为什么我会得到“类型示例化太深,可能是无限的”?

kb5ga3dv  于 2022-12-24  发布在  TypeScript
关注(0)|答案(5)|浏览(954)

Playground链接
我有这些一般性的定义:

type Module<P extends Payloads, C extends Children> = {
    payloads: P;
    children: C;
};

type Children = Record<string, any>; // some values will be nested Modules

type Payloads = Record<string, any>;

type ExtractPayloads<M extends Module<any, any>> = M extends Module<infer P, any> ? P : never;

type ExtractChildren<M extends Module<any, any>> = M extends Module<any, infer C> ? C : never;

基本上,模块是指定children类型的类型,children类型可以包含嵌套模块。
我有这样的类型,它可以根据模块的payload类型生成Action

type ModuleRootActions<
  MODULE extends Module<any, any>,
  PAYLOADS = ExtractPayloads<MODULE>,
> = {
  [NAME in keyof PAYLOADS]: {
    type: NAME;
    payload: PAYLOADS[NAME];
  };
}[keyof PAYLOADS];

接下来,我有一个递归类型,它可以帮助我为Module树中的所有Module(例如,包括它的子模块和孙模块等)生成Action

type AllModuleActions<
  MODULE extends Module<any, any>,
  CHILDREN = ExtractChildren<MODULE>,
> =
  | ModuleRootActions<MODULE>
  | {
      [KEY in keyof CHILDREN]: CHILDREN[KEY] extends Module<any, any>
        ? AllModuleActions<CHILDREN[KEY]>
        : never;
    }[keyof CHILDREN];

最后,我举几个具体的例子:

type C = Module<{
  "incrementBy": number;
}, {}>;

type B = Module<{
  "setIsSignedIn": boolean;
}, {
  c: C;
}>;

type A = Module<{
  "concat": string;
  "setIsDarkMode": boolean;
}, {
  b: B;
}>;

到目前为止,我的所有类型都是正确的--我已经手动验证了这一点。现在,我正在编写一个函数,它接受泛型ModuleAction。我可以成功地定义这些类型:

type ConcreteAction<M extends Module<any, any>> = AllModuleActions<M>;

const concreteAction: ConcreteAction<A> = {
  type: "concat",
  payload: "str",
}

但是一旦我试图把它们放在泛型函数中,我就会得到标题中的错误。

const composedReducer = <MODULE extends Module<any, any>>(
  action: AllModuleActions<MODULE>,
) => {
  if (action) {

  }
};

在Playground链接中,您会注意到action有错误:"类型示例化太深,可能是无限的"。我认为发生这种情况是因为MODULE类型是泛型的,并且在Module定义中可能有循环,即使从语义上我知道它是一棵树。
我该如何修正这个错误?有没有办法告诉编译器这个图永远是一棵树,并且永远不包含无限循环?

4dbbbstv

4dbbbstv1#

AllModuleActions<M>类型是递归的,编译器不能很好地处理它,你是indexing任意深入到一个对象类型,一次产生一个大的联合类型;在here之前,当生成一个对象类型的所有虚线路径时,我遇到过这个问题(例如,{a: {b: string, c: number}}变成"a.b" | "a.c")。
M是一个 * specific * 类型时,在计算AllModuleActions<M>这样的值时,通常不会看到这个问题;但是当它是一个未指定的generic类型参数(或者一个依赖于这样的类型参数的类型)时,你可能会遇到麻烦。你可能会看到那个"过分深"的错误。更糟糕的是,编译器往往会陷入困境,CPU使用率激增,IDE速度变慢。我不知道为什么会发生这种情况。
也许最好的建议是不要构建这样的类型。如果你必须这样做,那么我发现了一些方法来帮助你,但是它们并不是万无一失的:
有时候你可以通过将这些类型中的一个改写为分布式条件类型来使编译器 * 延迟 * 对它的求值。如果M是一个泛型类型参数,而AllModuleActions<M>给你带来麻烦,那么M extends any ? AllModuleActions<M> : never可能不会:

const generic = <M extends Module<any, any>>(
  action: M extends any ? AllModuleActions<M> : never, // <-- defer
) => {
  action; // okay
};

如果这不起作用,你可以尝试显式地限制你的递归类型的深度,这样在默认情况下,事情只会下降三到四个层次:

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

type AllModuleActions<M extends Module<any, any>, D extends Prev[number] = 4> =
  [D] extends [never] ? never :
  | ModuleRootActions<M>
  | {
    [K in keyof M["children"]]: M["children"][K] extends Module<any, any>
    ? AllModuleActions<M["children"][K], Prev[D]>
    : never;
  }[keyof M["children"]];

这和你的类似,只是我们增加了一个D参数,它(默认)从4开始,每次AllModuleActions求值时减小(注意Prev[4]3Prev[3]2),直到最后达到never,递归类型跳出:

const generic = <M extends Module<any, any>>(
  action: AllModuleActions<M>
) => {
  action; // okay
};

这些变通方法对于特定的用例可能有帮助,也可能没有帮助,并且可能存在可观察到的副作用(例如,类型可能不相同;类型推断可能表现不同;显示的quickinfo可能是更复杂的),所以要小心!
Playground代码链接

dfuffjeb

dfuffjeb2#

确保您没有旧的Typescript版本,因为旧版本在类型推断方面存在一些bug
对于我来说,从3.9更新到4.6.3修复了这个问题。

mpgws1up

mpgws1up3#

好吧,这个错误很常见,所以我不知道我的帖子是否真的会有帮助,但我在一个使用Typescript和vueI18n的Vue项目中遇到了这个问题。当我在 *.vue文件之外的实用程序代码中引用VueI18n.global.t方法时,编译器引发了这个错误。我使用了Vue3、Vuetify和vueI18n,我的文件看起来像这样

import { createI18n } from 'vue-i18n'
import en from 'vuetify/lib/locale/en'
import fr from 'vuetify/lib/locale/fr'

const messages = {
  en: {
    ...require('@/locales/en.json'),
    $vuetify: en
  },
  fr: {
    ...require('@/locales/fr.json'),
    $vuetify: fr
  }
}

export default createI18n({
    locale: localStorage.getItem('locale') || process.env.VUE_APP_I18N_LOCALE || 'en',
    fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
    messages
  })

经过几个小时的努力,我发现了这个非常简单的例子here。所以我更新了我的文件,现在它编译正确了。

import { createI18n, FallbackLocale } from 'vue-i18n'
import en from '@/locales/en.json'
import fr from '@/locales/fr.json'

const locale: string = localStorage.getItem('locale') || process.env.VUE_APP_I18N_LOCALE || 'en'
const fallbackLocale: FallbackLocale = process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en'

export default createI18n({
  locale: locale,
  fallbackLocale: fallbackLocale,
  messages: {
    en,
    fr,
  }
})
7uzetpgm

7uzetpgm4#

添加到tsconfig.json:

"ts-node": {
      "swc": true
}

然后安装这个:
npm i -D @swc/core @swc/helpers再生器运行时
对我很有效。

7uhlpewt

7uhlpewt5#

可以通过检查递归类型中的类型是否为any来避免这个错误。
例如,我们希望提取对象中使用的所有值。

type AllValues<T extends object, K = keyof T> = K extends keyof T
  ? T[K] extends object
    ? AllValues<T[K]>
    : T[K]
  : never;

interface Data {
  num: number
  deep: {
    deep: {
      str: string
    }
  }
}

type A = AllValues<Data> // string | number
type B = AllValues<any> // Type instantiation is excessively deep and possibly infinite

见操场。
TypeScript抛出一个错误,因为AllValues类型被any类型无限调用。为了避免它,在递归类型AllValues中,我们应该为any类型添加一个检查,如下所示:

type IsAny<T> = unknown extends T & string ? true : false;

type AllValues<T extends object, K = keyof T> = IsAny<T> extends true
  ? string
  : K extends keyof T
  ? T[K] extends object
    ? AllValues<T[K]>
    : T[K]
  : never;

interface Data {
  num: number
  deep: {
    deep: {
      str: string
    }
  }
}

type A = AllValues<Data> // string | number
type B = AllValues<any> // string

见操场。
让我们回到你的例子,你所要做的就是在类型AllModuleActions中添加一个any的检查。

type IsAny<T> = unknown extends T & string ? true : false;

type AllModuleActions<
  MODULE extends Module<any, any>,
  CHILDREN = ExtractChildren<MODULE>
> = IsAny<MODULE> extends true
  ? any
  : ...

见操场。
在前面,作为一种解决方案,您可以限制递归类型(代码为type Prev = [never, 0, 1, 2, ...])的最大嵌套调用数,这是可行的,但在这种情况下,生成的类型声明变得非常庞大。

相关问题