给出接口或类A和B,它们具有一个共同的x1
字段
interface A {
a1: number;
x1: number; // <<<<
}
interface B{
b1: number;
x1: number; // <<<<
}
给出实现a和b
let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};
即使b1不是A的一部分,Typescript也允许这样做:
let partialA: Partial<A> = b;
您可以在此处找到有关发生这种情况的解释:Why Partial accepts extra properties from another type?
Partial的一种替代方法是仅接受其他类型的字段,而不接受其他字段(尽管不需要所有字段)?像StrictPartial
一样?
这一直在我的代码库中引起很多问题,因为它根本没有检测到错误的类作为参数传递给了函数。
答案 0 :(得分:2)
您真正想要的东西叫做exact types,其中类似“ Exact<Partial<A>>
”的东西会在所有情况下防止多余的属性。但是TypeScript不直接支持精确类型(至少从TS3.5开始不支持),因此没有很好的方法将Exact<>
表示为具体类型。您可以模拟精确类型作为通用约束,这意味着突然之间,与它们相关的所有事物都需要变得通用而不是具体。
类型系统将类型视为正确的唯一时间是对“新鲜对象文字”进行excess property checks时,但是在某些极端情况下,这种情况不会发生。这些极端情况之一就是您的类型很弱(没有强制性属性),例如Partial<A>
,因此我们完全不能依靠多余的属性检查。
在一条评论中,您说您想要一个其构造函数采用Exact<Partial<A>>
类型参数的类。像
class Example {
constructor(public partialA: Exact<Partial<A>>) {} // doesn't compile
}
我将向您展示如何获得类似的东西,以及一些注意事项。
让我们定义通用类型别名
type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
这需要一个类型T
和一个候选类型U
,我们要确保它是“完全T
”。它返回一个类似于T
的新类型,但具有与never
中的额外属性相对应的额外U
值属性。如果我们将其用作对U
的约束,例如U extends Exactly<T, U>
,那么我们可以保证U
与T
匹配并且没有额外的属性。
例如,假设T
是{a: string}
,而U
是{a: string, b: number}
。然后Exactly<T, U>
等效于{a: string, b: never}
。请注意,U extends Exactly<T, U>
为假,因为它们的b
属性不兼容。 U extends Exactly<T, U>
为true的唯一方法是如果U extends T
但没有额外的属性。
所以我们需要一个泛型构造函数,例如
class Example {
partialA: Partial<A>;
constructor<T extends Exactly<Partial<A>, T>>(partialA: T) { // doesn't compile
this.partialA = partialA;
}
}
但是您不能这样做,因为构造函数不能在类声明中拥有自己的类型参数。这是泛型类和泛型函数之间的交互的unfortunate consequence,因此我们将不得不解决它。
这里有三种方法。
1:将类设为“不必要的泛型”。这使得构造函数可以按需要进行泛型设置,但是会导致此类的具体实例携带指定的泛型参数:
class UnnecessarilyGeneric<T extends Exactly<Partial<A>, T>> {
partialA: Partial<A>;
constructor(partialA: T) {
this.partialA = partialA;
}
}
const gGood = new UnnecessarilyGeneric(a); // okay, but "UnnecessarilyGeneric<A>"
const gBad = new UnnecessarilyGeneric(b); // error!
// B is not assignable to {b1: never}
2:隐藏构造函数,并改用静态函数创建实例。该静态函数可以是通用的,而类不是:
class ConcreteButPrivateConstructor {
private constructor(public partialA: Partial<A>) {}
public static make<T extends Exactly<Partial<A>, T>>(partialA: T) {
return new ConcreteButPrivateConstructor(partialA);
}
}
const cGood = ConcreteButPrivateConstructor.make(a); // okay
const cBad = ConcreteButPrivateConstructor.make(b); // error!
// B is not assignable to {b1: never}
3:使类没有确切的约束,并为其指定一个虚拟名称。然后使用类型声明从旧的类构造器创建一个新的类构造器,该构造器具有所需的通用构造器签名:
class _ConcreteClassThatGetsRenamedAndAsserted {
constructor(public partialA: Partial<A>) {}
}
interface ConcreteRenamed extends _ConcreteClassThatGetsRenamedAndAsserted {}
const ConcreteRenamed = _ConcreteClassThatGetsRenamedAndAsserted as new <
T extends Exactly<Partial<A>, T>
>(
partialA: T
) => ConcreteRenamed;
const rGood = new ConcreteRenamed(a); // okay
const rBad = new ConcreteRenamed(b); // error!
// B is not assignable to {b1: never}
所有这些都应该工作以接受“确切的” Partial<A>
实例并拒绝具有额外属性的事物。好吧,差不多。
他们拒绝具有 known 额外属性的参数。类型系统实际上并不能很好地表示精确类型,因此任何对象都可以具有编译器不知道的额外属性。这是超类的子类可替换性的本质。如果我可以先做class X {x: string}
,然后再做class Y extends X {y: string}
,那么即使Y
一无所知,X
的每个实例也是X
的实例。 y
属性。
因此,您始终可以扩展对象类型以使编译器忽略属性,这是正确的:(过多的属性检查通常会使此操作变得更加困难,但在某些情况下并非如此)
const smuggledOut: Partial<A> = b; // no error
我们知道可以编译,我无法做任何改变。这意味着即使使用上述实现,您仍可以在以下位置传递B
:
const oops = new ConcreteRenamed(smuggledOut); // accepted
防止这种情况的唯一方法是某种类型的运行时检查(通过检查Object.keys(smuggledOut)
。因此,如果这样的检查确实对接受带有额外属性的东西有害,则最好在类构造函数中建立这样的检查。或者,您可以以这样一种方式构建类,即在不破坏附加属性的情况下静默丢弃这些附加属性,无论哪种方式,上述类定义都可以将类型系统推向精确类型的方向,即至少现在是这样。
希望有所帮助;祝你好运!