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

Dou*_*los 6 partial typescript

给定具有x1共同字段的接口或类 A 和 B

interface A {
  a1: number;
  x1: number;  // <<<<
}

interface B{
  b1: number;
  x1: number;  // <<<<
}
Run Code Online (Sandbox Code Playgroud)

并给出实现 a 和 b

let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};
Run Code Online (Sandbox Code Playgroud)

打字稿允许这样做,即使 b1不是A 的一部分:

let partialA: Partial<A> = b;
Run Code Online (Sandbox Code Playgroud)

你可以在这里找到为什么会发生这种情况的解释:为什么 Partial 接受来自另一种类型的额外属性?

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

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

jca*_*alz 11

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

类型系统将类型视为精确的唯一时间是它对“新鲜对象文字”进行过多的属性检查时,但有一些边缘情况不会发生这种情况。这些边缘情况之一是当您的类型很弱(没有强制性属性)时,例如Partial<A>,因此我们根本不能依赖过多的属性检查。

在评论中你说你想要一个类,它的构造函数接受一个 type 的参数Exact<Partial<A>>。就像是

class Example {
   constructor(public partialA: Exact<Partial<A>>) {} // doesn't compile
}
Run Code Online (Sandbox Code Playgroud)

我将向您展示如何获得类似的东西,以及沿途的一些注意事项。


让我们定义泛型类型别名

type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
Run Code Online (Sandbox Code Playgroud)

这需要一个类型T和一个我们要确保“完全正确”的候选类型。它返回一个新类型,是喜欢,但额外对应于额外的属性-valued性能。如果我们使用它作为对, like的约束,那么我们可以保证匹配并且没有额外的属性。UTTneverUUU extends Exactly<T, U>UT

例如,想象一下Tis{a: string}Uis {a: string, b: number}。然后Exactly<T, U>变成等价于{a: string, b: never}。请注意,这U extends Exactly<T, U>是错误的,因为它们的b属性不兼容。唯一U extends Exactly<T, U>正确的方法是 ifU extends T但没有额外的属性。


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

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

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

以下是三种方法。

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}
Run Code Online (Sandbox Code Playgroud)

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}
Run Code Online (Sandbox Code Playgroud)

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}
Run Code Online (Sandbox Code Playgroud)

所有这些都应该可以接受“精确”Partial<A>实例并拒绝具有额外属性的事物。嗯,差不多。


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

所以你总是可以扩大一个对象类型,让编译器忘记属性,这是有效的:(在某些情况下,过度的属性检查往往会使这变得更加困难,但不是在这里)

const smuggledOut: Partial<A> = b; // no error
Run Code Online (Sandbox Code Playgroud)

我们知道可以编译,而我所做的任何事情都无法改变它。这意味着即使使用上述实现,您仍然可以传入B

const oops = new ConcreteRenamed(smuggledOut); // accepted
Run Code Online (Sandbox Code Playgroud)

防止这种情况的唯一方法是使用某种运行时检查(通过检查Object.keys(smuggledOut). 因此,如果接受具有额外属性的东西确实有害,那么最好在类构造函数中构建这样的检查。或者,您可以在这样它就会默默地丢弃额外的属性而不会被它们损坏。无论哪种方式,上面的类定义都可以将类型系统推向精确类型的方向,至少现在是这样。

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

代码链接