类型系统不适用于对额外对象键的此类限制。 TypeScript 中的类型不是exact:如果一个对象是A 类型,并且你添加了更多A 的定义中没有提到的属性,那么该对象仍然是A 类型。这本质上是支持类继承所必需的,其中子类可以向超类添加属性。
编译器唯一将类型视为精确的情况是当您使用“新”对象字面量(即尚未分配给任何东西的字面量)并将其传递给需要对象类型的东西时。这被称为excess property checking,它是一种解决语言中缺少确切类型的方法。您希望对像 test 这样的“非新鲜”对象进行过多的属性检查,但这不会发生。
TypeScript 没有精确类型的具体表示;您不能使用类型 T 并从中生成 Exact<T>。但是您可以使用generic constraint 来获得这种效果。给定一个类型 T 和一个类型为 U 的对象,你希望它们符合不可表示的 Exact<T> 类型,你可以这样做:
type Exactly<T, U extends T> = {[K in keyof U]: K extends keyof T ? T[K] : never};
type IsExactly<T, U extends T> = U extends Exactly<T, U> ? true : false;
const testGood = {
g: 1
}
type TestGood = IsExactly<Class2, typeof testGood>; // true
const testBad = {
g: 6,
a: '6',
};
type TestBad = IsExactly<Class2, typeof testBad>; // false
因此编译器能够判断typeof testGood 是“Exactly<Class2, typeof testGood>”,而typeof testBad 不是Exactly<Class2, typeof testBad>。我们可以使用它来构建一个通用函数来做你想做的事。 (在您的情况下,您需要 ExactlyPartial<T, U> 而不是 Exactly<T, U>,但它非常相似......只是不要将 U 限制为扩展 T)。
不幸的是,您的函数在T 中已经是泛型的,需要准确的类型。而你手动指定T,泛型函数需要推断U的类型。 TypeScript 不允许 partial type parameter inference。您必须手动指定函数中的所有类型参数,或者必须让编译器推断函数中的所有类型参数。所以有解决方法:
一种是将您的函数拆分为curried 函数,其中第一个泛型函数允许您指定T,返回的泛型函数推断U。它看起来像这样:
class Testing {
static testIt<T>(): <U extends { [K in keyof U]:
K extends keyof T ? T[K] : never
}> (val: U) => void {
return () => { }
}
}
Testing.testIt<Class2>()(testBad); // error, prop "a" incompatible
Testing.testIt<Class2>()(testGood); // okay
这可以按您的预期工作,但会影响运行时,因为您必须在运行时无缘无故地调用 curried 函数。
另一种解决方法是传递一个值,从中可以推断出T 给函数。由于您不需要这样的值,因此这本质上是一个虚拟参数。同样,它具有运行时影响,因为您必须传入一个未使用的值。 (您提到您实际上可能在运行时使用了这样的值,在这种情况下,这不再是一种解决方法,而是建议的解决方案,因为无论如何您都需要传递一些东西,并且在您的代码中手动指定 T示例是一条红鲱鱼。)它看起来像这样:
class Testing {
static testIt<T, U extends { [K in keyof U]:
K extends keyof T ? T[K] : never
}>(ctor: new (...args: any) => T, val: U) {
// not using ctor in here, so this is a dummy value
}
}
Testing.testIt(Class2, testBad); // error, prop "a" incompatible
Testing.testIt(Class2, testGood); // okay
我能想到的第三种解决方法是只使用类型系统来表示柯里化函数返回的结果,而不实际调用它。它完全没有运行时影响,这使得它更适合为现有的 JS 代码提供类型,但使用起来有点笨拙,因为你必须断言 Testing.testIt 行为正确。它看起来像这样:
interface TestIt<T> {
<U extends { [K in keyof U]: K extends keyof T ? T[K] : never }>(val: U): void;
}
class Testing {
static testIt(val: object) {
// not using ctor in here, so this is a dummy value
}
}
(Testing.testIt as TestIt<Class2>)(testBad); // error, prop "a" incompatible
(Testing.testIt as TestIt<Class2>)(testGood); // okay
好的,希望其中一个对您有用。祝你好运!
Link to code