typescript 递归类型报告“类型示例化太深,可能是无限的”,

qij5mzcb  于 2023-05-30  发布在  TypeScript
关注(0)|答案(1)|浏览(218)

我正在编写一个与Jest一起使用的实用程序,它在访问或调用/调用属性时惰性地创建一个对象模拟。
基本上,如果一个属性像方法一样被调用,它将为该对象路径创建或重用一个jest.fn()。如果一个属性被作为一个值访问(并且该属性之前没有被显式地设置),它将在该属性上创建另一个动态模拟对象(递归地)。

const foo = new DynamicMock()

foo.bar.baz.buzz('Hello') // buzz() becomes a jest.fn() when called
console.log(foo.bar.baz.buzz.mock.calls[0][0]) // 'Hello'

我已经写了实用程序,它的工作原理与预期一样(它的供电代理对象),但我有麻烦键入它。
该实用程序的目标是创建一个可以满足对象依赖关系(如接口)的类型,并使测试期间模拟依赖关系的过程更容易。

interface Foo {
  bar(): void
}

let fooMock: DynamicMock<Foo>

beforeEach(() => {
  fooMock = new DynamicMock()
})

it('Should', () => {
  fooMock.bar // Should have type checking/completion for this property
})

我已经创建了一个类型签名,它可以工作,但是当模拟的类型包含递归类型时,它会中断。

export type DynamicMock<T> = {
    [K in keyof T]: T[K] &
        (T[K] extends (...args: infer A) => infer B ? jest.Mock<B, A> : DynamicMock<T[K]>);
};

export type DynamicMockConstructor = {
    new <T>(): DynamicMock<T>;
    extend<T>(constructor: unknown): new <U extends T>() => DynamicMock<U>;
};

export const DynamicMock: DynamicMockConstructor;
declare module externalPackage {
    export type JSONValue = null | void
        | boolean | number | string | Array<JSONValue> | JSONObject;

    export type JSONObject = {
        [key: string]: JSONValue;
    };

    export interface Bar {
        meta: JSONObject
    }

    export interface Foo {
        getBars(): ReadonlyArray<Bar>
    }
}

const mockFoo = new DynamicMock<externalPackage.Foo>()
const mockBar = new DynamicMock<externalPackage.Bar>()

mockFoo.getBars.mockReturnValue([mockBar]) // "Type instantiation is excessively deep and possibly infinite."

这将导致Type instantiation is excessively deep and possibly infinite.错误(由于递归JSONObject类型)。
我不能从外部软件包更改代码,所以我想知道是否有什么我可以做我的DynamicMock类型签名,以改善这种情况?
TypeScript Playground

uemypmqf

uemypmqf1#

一种可能的方法是在递归类型定义中编码一个深度限制器,这样当它与其他递归或深度嵌套类型交互时,在某个深度后“放弃”。下面是一种在示例代码中执行此操作的方法:

type DynamicMock<T, D extends number = 9, DA extends any[] = []> = {
    [K in keyof T]: T[K] &
    (T[K] extends (...args: infer A) => infer B ? jest.Mock<B, A> :
        D extends DA['length'] ? any : DynamicMock<T[K], D, [0, ...DA]>);
};

generic类型参数D是一个数字文字类型,对应于我们在放弃之前要递归到的最大深度。我将它默认为9,但您可以指定其他数字。
编译器不能直接对数字文字类型进行数学运算(有关相关特性请求,请参阅ms/TS#26382),因此为了“计数到D”,我们使用可变元组类型来将值前置到元组,可以检查其length。因此,虽然“1 + 8 = 9”不是直接可表示的,但您可以说“将元素前置到长度为8的元组会产生长度为9的元组”。因此,我们使用累加器类型参数DA来存储这个元组。
无论如何,当DA的长度等于D时,我们已经达到了基本情况,我们返回any而不是递归。否则,我们将DA延长一个元素([0, ...DA]是一个新的元组类型,其前缀为00可以是任何你想要的,因为我们只关心元组的长度。我选择0是因为它是一个名字很短的类型)和递归。
如果我在您的代码示例上尝试此操作,错误将按预期消失。
不过,这种方法是否真的适用于所有必需的用例并不明显。深度递归的实用程序类型通常具有奇怪的边缘情况。深度限制器本身可能会与其他递归类型产生不良的交互,您需要调整或重构它。我所能建议的是,在将这些深度递归的实用程序类型包含到任何生产级代码库中之前,应该针对广泛的潜在用例对它们进行彻底的测试。
Playground链接到代码

相关问题