有没有Partial的替代方法,它只接受其他类型的字段而别无其他?

时间:2019-06-15 01:13:43

标签: typescript partial

给出接口或类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一样?

这一直在我的代码库中引起很多问题,因为它根本没有检测到错误的类作为参数传递给了函数。

1 个答案:

答案 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>,那么我们可以保证UT匹配并且没有额外的属性。

例如,假设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)。因此,如果这样的检查确实对接受带有额外属性的东西有害,则最好在类构造函数中建立这样的检查。或者,您可以以这样一种方式构建类,即在不破坏附加属性的情况下静默丢弃这些附加属性,无论哪种方式,上述类定义都可以将类型系统推向精确类型的方向,即至少现在是这样。

希望有所帮助;祝你好运!

Link to code