在通用测试函数中索引类实例时如何解决 TypeScript 错误 ts(2536)?

问题描述 投票:0回答:1

我在解决 TypeScript 类型错误时遇到困难

ts(2536)
。 对于上下文,我有一些类来存储各种组件的配置值。 其中一个组件是下面的
AWSConfig
类。

class AWSConfig {
    readonly accessKeyID: string;
    readonly secretAccessKey: string;
    readonly region: string;

    constructor() {
        // ... code to validate environment variables
        // assign validated variables to the respective properties
        this.accessKeyID = '...';
        this.secretAccessKey = '...';
        this.region = '...';
    }

    // ... other methods
}

对这些配置类进行单元测试的逻辑是相似的,只是测试用例有所不同。 我正在编写一个通用测试函数,可用于为团队的其他成员测试这些类。 在通用测试功能中存在一个我无法正确解决的类型错误。 函数签名如下。

function configTests<
    T extends ConfigClass<T>,
    P extends ClassProperties<T> = ClassProperties<T>,
>(
    Config: T,
    cases: ReadonlyArray<ConfigTestCases<P>>,
) {
    // ... simplified code for brevity
    cases.forEach(({ property, tests }) => {
        const config = new Config();

        tests.forEach(({ expected }) => {
            const result = config[property] === expected;

            console.log(result);
        });
    });
}

这使得可以按如下方式编写测试。

configTests(
    AWSConfig,
    [
        {
            property: 'accessKeyID',
            tests: [
                {
                    expected: '...',
                },
                // ... other cases
            ],
        },
        // ... other properties
    ],
);

configTests
函数中,导致错误的行如下。

const config = new Config();

const result = config[property] === expected; // ts(2536) error at config[property]

错误发生在

config[property]
处,如
Type 'Extract<keyof P, string>' cannot be used to index type 'InstanceType<T>'.ts(2536)
。 我使用
const config = new Config() as Record<string, unknown>
删除了错误。 我认为这些类型并不完全正确,但我不知道如何解决这个问题。

完整代码可在 TypeScript Playground 此处获取。

typescript
1个回答
0
投票

示例中的代码比其工作所需的更复杂。特别是,

interface ConfigClass<T extends ConfigClass<T>> {
    new(): InstanceType<T>;
}

是一个递归有界通用类型。虽然在某些情况下此类类型很有用,但在这里看起来您只是试图说

ConfigClass<T>
是一个 构造签名,它构造
ConfligClass<T>
构造的实例类型的实例。对于任何构造函数类型都是如此,而
T
约束所做的一切只是让编译器变得更加困难。 您可以用
new () => object
替换它,因为它有很多好处。事实上,我认为你根本不需要命名类型。


我们不要在 constructor 类型中使事物通用,而是将代码重写为仅在 instance 类型中通用。 您对构造函数类型的所有使用都是通过

InstanceType<T>
实用程序类型返回到实例类型。这是一个条件类型,当
T
是泛型时,TypeScript不知道如何分析它。虽然人类可以理解任意
InstanceType<T>
T
(特别是给定类型名称),但 TypeScript 仅当
T
是某种特定类型时才“理解”它。它无法抽象操作来“查看”一般发生的情况。因此,在这种情况下,您可能会遇到 TypeScript 无法验证兼容性的错误。


因此,让我们在实例类型中使事物变得通用。在

ClassProperties<T>
中,
T
只是实例类型本身,因此简化为:

type ClassProperties<T> = {
    [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

(请注意,您的

AnyFunction
类型实际上不是 任何函数,因为您创建了其余参数
unknown[]
,这是 限制性,不是宽松的。请参阅 TypeScript `unknown` 不允许非 -函数参数中的未知类型。如果你想实际排除函数,你可以使用
Function
接口
,这对于输入实际变量来说是一件坏事,但在检查调用签名是否存在时,它是理想。)

configTests
中,您可以使用
T
作为实例类型。而且重要的是,您不需要单独的泛型
P
,因为您可以直接使用预期的类型:

function configTests<T extends object>(
    Config: new () => T,
    cases: ReadonlyArray<ConfigTestCases<keyof ClassProperties<T>>>,
) {
    cases.forEach(({ property, tests }) => {
        const config = new Config();

        tests.forEach(({ expected }) => {
            const result = config[property] === expected;
            console.log(result);
        });
    });
}

这可以编译。 TypeScript 知道

keyof ClassProperties<T>
是可分配给
keyof T
的,这就是开始工作所需的全部内容。 TypeScript 不必尝试分析
InstanceType<T>
,更重要的是,它不必尝试理解
P
如何/是否与
T
相关。在您的代码中,
P
只是默认
ClassProperties<T>
,并且被约束
ClassProperties<T>
,但这并不意味着它与ClassProperties<T>
相同
。它可以是任何子类型,例如
ClassProperties<T> & {someRandomProp: string}
。 这种可能性意味着 TypeScript 确实无法确定
property
config
的键。 您的代码按原样允许这样做:

configTests<typeof AWSConfig,
    {
        accessKeyID: string,
        secretAccessKey: string,
        region: string,
        someRandomProperty: string
    }>(AWSConfig,
        [
            {
                property: 'someRandomProperty',
                tests: [
                    {
                        expected: '...',
                    },
                ],
            },
        ],
    );

而且没有理由允许这样做。拥有额外的泛型类型参数会让事情变得更糟。


好吧,让我们测试一下:

configTests(
    AWSConfig,
    [
        {
            property: 'accessKeyID',
            tests: [
                {
                    expected: '...',
                },
            ],
        },
    ],
);

这仍然有效,如果您检查调用,您会看到

T
已被推断为
AWSConfig
。你不能把
someRandomProperty
放在那里,因为没有第二种类型的参数。因此,如果您尝试,就会收到错误消息,要么是因为
someRandomProperty
不在
keyof T
中,要么是因为
AWSConfig
构造函数不会创建具有
someRandomProperty
的实例:

configTests<AWSConfig & { someRandomProperty: string }>(
    AWSConfig, // error here!
    [
        {
            property: 'someRandomProperty',
            tests: [
                {
                    expected: '...',
                },
            ],
        },
    ],
);

Playground 代码链接

© www.soinside.com 2019 - 2024. All rights reserved.