是否有替代 Partial 的方法来只接受来自另一种类型的字段而不接受其他类型的字段?

Is there an alternative to Partial to accept only fields from another type and nothing else?

给定接口或 classA 和 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};

Typescript 允许这样做,即使 b1 不是 A:

的一部分
let partialA: Partial<A> = b;

You can find the explaination of why this happens here:

是否可以替代 Partial 以仅接受另一种类型的字段而不接受其他类型的字段(虽然不需要所有字段)?像 StrictPartial?

这样的东西

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

您真正想要的是 exact types,其中类似于“Exact<Partial<A>>”的内容可以在所有情况下防止过多的属性。但是 TypeScript 不直接支持精确类型(至少从 TS3.5 开始不支持)所以没有好的方法将 Exact<> 表示为具体类型。您可以 模拟 精确类型作为通用约束,这意味着突然之间处理它们的所有内容都需要变得通用而不是具体。

类型系统将类型视为精确类型的唯一一次是在 excess property checks 上 "fresh object literals",但在某些边缘情况下不会发生这种情况。其中一种极端情况是当您的类型较弱(没有强制属性)时,例如 Partial<A>,因此我们根本不能依赖过多的 属性 检查。

并且在评论中你说你想要一个 class 其构造函数采用 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 我们要确保的是 "exactly T"。它 returns 一种类似于 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> 为真的唯一方法是如果 U extends T 但没有额外的属性。


所以我们需要一个通用构造函数,比如

class Example {
  partialA: Partial<A>;
  constructor<T extends Exactly<Partial<A>, T>>(partialA: T) { // doesn't compile
    this.partialA = partialA;
  }
}

但是你不能那样做,因为构造函数不能在 class 声明中有自己的类型参数。这是泛型 classes 和泛型函数之间交互的 unfortunate consequence,因此我们将不得不解决它。

这里有三种方法。

1:制作class"unnecessarily generic"。这使得构造函数按需要通用,但导致此 class 的具体实例携带指定的通用参数:

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 不是:

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没有确切的约束,并给它一个虚拟名称。然后使用类型断言从具有您想要的通用构造函数签名的旧构造函数创建新的 class 构造函数:

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}

所有这些都应该能够接受 "exact" Partial<A> 实例并拒绝具有额外属性的东西。嗯,差不多。


他们拒绝具有 已知 额外属性的参数。类型系统并不能很好地表示确切的类型,因此任何对象都可以具有编译器不知道的额外属性。这就是subclasses可替代superclasses的本质。如果我可以做 class X {x: string} 然后 class Y extends X {y: string},那么 Y 的每个实例也是 X 的实例,即使 X 不知道任何关于y 属性。

因此,您始终可以扩大对象类型以使编译器忘记属性,这是有效的:(在某些情况下,过度 属性 检查往往会使这变得更加困难,但此处不会)

const smuggledOut: Partial<A> = b; // no error

我们知道编译,我做任何事情都无法改变它。这意味着即使使用上面的实现,你仍然可以传递一个 B in:

const oops = new ConcreteRenamed(smuggledOut); // accepted

防止这种情况发生的唯一方法是进行某种运行时检查(通过检查 Object.keys(smuggledOut)。因此,如果它确实对接受具有额外属性的东西。或者,你可以构建你的 class 以这样一种方式,它会默默地丢弃额外的属性而不会被它们损坏。无论哪种方式,上面的 class 定义大约是至少现在可以将类型系统推向精确类型的方向。

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

Link to code