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

s4n0splo  于 2023-02-10  发布在  TypeScript
关注(0)|答案(2)|浏览(133)

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

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的方法,只接受来自另一种类型的字段,而不接受其他任何字段(尽管不需要所有字段)?
这在我的代码库中造成了很多问题,因为它根本无法检测到错误的类作为参数传递给了函数。

tjrkku2a

tjrkku2a1#

您真正需要的是exact types,其中类似于“Exact<Partial<A>>“的内容将在所有情况下防止多余的属性。(至少在TS3.5中不是这样),所以没有好的方法将Exact<>表示为具体类型。您可以将精确类型 * 模拟 * 为泛型约束,这意味着突然之间,与它们有关的一切都需要变得通用而不是具体。
类型系统认为类型是精确的唯一情况是对“新鲜对象常量”进行额外的属性检查,但也有一些边缘情况,这种情况不会发生。其中一个边缘情况是当你的类型很弱(没有强制属性),比如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和我们想要确保“确切地为T“的 * 候选 * 类型U。它返回类似于T但具有对应于U中的额外属性的额外never值属性的新类型。如果我们将此用作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;
  }
}

但是你不能这样做,因为构造函数不能在类声明中有自己的类型参数,这是泛型类和泛型函数交互的一个不幸的结果,所以我们必须解决它。
这里有三种方法。
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:创建一个没有确切约束的类,并给予它一个伪名称。然后使用类型Assert从旧的类构造函数创建一个新的类构造函数,它具有你想要的泛型构造函数签名:

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))。因此,如果接受具有额外属性的内容确实会造成损害,那么在类构造函数中构建这样的检查是一个好主意。或者,您可以构建类,使其在不受额外属性损害的情况下默默地丢弃这些属性。无论哪种方式,以上的类定义差不多是类型系统可以被推向精确类型的方向的程度,至少现在是这样。
希望能有所帮助;祝你好运!
链接到代码

nwnhqdif

nwnhqdif2#

迟到了,但这对我很有效:

export type StrictPartial<Subset, Original> = {
[K in keyof Subset]:
    K extends keyof Original ?
        StrictPartial<Subset[K], Original[K]> 
    : Subset extends { [key: string]: Original } ?
        StrictPartial<Subset[K], Original>:never
};

function fn<T extends StrictPartial<T, {a:number,b:number}>>() {

}

fn<{a:number}>() //works
fn<{a:number,c:number}> //type error

相关问题