给定的接口(interface)或类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;
You can find the explaination of why this happens here: Why Partial accepts extra properties from another type?
是Partial的替代方法,它仅接受其他类型的字段,而不接受其他字段(尽管不需要所有字段)?像
StrictPartial
一样?这一直在我的代码库中引起很多问题,因为它根本无法检测到错误的类作为参数传递给了函数。
最佳答案
您真正想要的是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>
为false,因为它们的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>
实例,并拒绝具有额外属性的事物。好吧,差不多。它们拒绝具有已知额外属性的参数。类型系统实际上并不能很好地表示确切类型,因此任何对象都可以具有编译器不知道的额外属性。这是父类(super class)的子类可替换性的本质。如果我可以先做
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)
。因此,如果这样的检查确实有害于接受带有额外属性的东西,则最好在类构造函数中构建这样的检查。无论哪种方式,至少在目前为止,上述类定义都可以将类型系统推向精确类型的方向。希望能有所帮助;祝你好运!
Link to code
关于typescript - 有没有Partial的替代方法,它仅接受其他类型的字段,而别无其他?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56606614/