我在解决 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 此处获取。
示例中的代码比其工作所需的更复杂。特别是,
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: '...',
},
],
},
],
);