是否有 Partial 的替代方法来仅接受来自另一种类型的字段而不接受其他类型的字段? [英] Is there an alternative to Partial to accept only fields from another type and nothing else?

查看:39
本文介绍了是否有 Partial 的替代方法来仅接受来自另一种类型的字段而不接受其他类型的字段?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

接口 A {a1:数量;x1:数量;//<<<<}接口B{b1:编号;x1:数量;//<<<<}

并给出实现 a 和 b

令 a: A = {a1: 1, x1: 1};让 b: B = {b1: 1, x1: 1};

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

let partialA: Partial= b;

<块引用>

你可以在这里找到为什么会发生这种情况的解释:为什么是部分接受其他类型的额外属性?

是 Partial 的替代方案,只接受另一种类型的字段而不接受其他类型的字段(尽管不需要所有字段)?类似于 StrictPartial 的东西?

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

解决方案

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

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

在评论中,您说您想要一个类,其构造函数采用 Exact> 类型的参数.类似的东西

class 示例 {构造函数(public partialA: Exact<Partial<A>>) {}//不编译}

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

<小时>

让我们定义泛型类型别名

type Exactly= T &记录<排除<keyof U,keyof T>,从不>;

这需要一个类型 T 和一个 candidate 类型 U ,我们要确保完全是 T".它返回一个类似于 T 的新类型,但具有额外的 never 值属性,对应于 U 中的额外属性.如果我们把它作为对U的约束,比如U extends Exactly,那么我们可以保证U匹配T 并且没有额外的属性.

例如,假设 T{a: string}U{a: string, b: number}.然后 Exactly 就等价于 {a: string, b: never}.注意 U extends Exactly 是假的,因为它们的 b 属性是不兼容的.U extends Exactly 为真的唯一方法是 U 扩展 T 但没有额外的属性.

<小时>

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

class 示例 {部分A:部分A;构造函数<T extends Exactly<Partial<A>, T>>(partialA: T) {//不编译this.partialA = partialA;}}

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

以下是三种方法.

1:使类不必要地通用".这使得构造函数按照需要泛型,但导致此类的具体实例携带指定的泛型参数:

class UnnecessouslyGeneric, T>>{部分A:部分A;构造函数(部分A:T){this.partialA = partialA;}}const gGood = new UnnecessouslyGeneric(a);//好的,但是不必要的泛型"const gBad = new UnnecessouslyGeneric(b);//错误!//B 不能赋值给 {b1: never}

2:隐藏构造函数并使用静态函数来创建实例.这个静态函数可以是泛型的,而类不是:

class ConcreteButPrivateConstructor {私有构造函数(public partialA: Partial) {}public static make, T>>(partialA: T) {返回新的 ConcreteButPrivateConstructor(partialA);}}const cGood = ConcreteButPrivateConstructor.make(a);//好的const cBad = ConcreteButPrivateConstructor.make(b);//错误!//B 不能赋值给 {b1: never}

3:使类没有确切的约束,并给它一个虚拟名称.然后使用类型断言从旧的类构造函数中创建一个新的类构造函数,该类构造函数具有您想要的通用构造函数签名:

class _ConcreteClassThatGetsRenamedAndAsserted {构造函数(公共部分A:部分){}}接口 ConcreteRenamed 扩展 _ConcreteClassThatGetsRenamedAndAsserted {}const ConcreteRenamed = _ConcreteClassThatGetsRenamedAndAsserted as new <T完全扩展<部分>,T>>(部分A:T) =>具体重命名;const rGood = new ConcreteRenamed(a);//好的const rBad = new ConcreteRenamed(b);//错误!//B 不能赋值给 {b1: never}

所有这些都应该能够接受精确的"Partial 实例并拒绝具有额外属性的事物.嗯,差不多了.

<小时>

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

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

const smuggledOut: Partial= b;//没有错误

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

中传递 B

const oops = new ConcreteRenamed(smuggledOut);//接受

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

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

链接到代码

Given interfaces or classes A and B with a x1 field in common

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

interface B{
  b1: number;
  x1: number;  // <<<<
}

And given the implementations a and b

let a: A = {a1: 1, x1: 1};
let b: B = {b1: 1, x1: 1};

Typescript allows this, even though b1 is not part of A:

let partialA: Partial<A> = b;

You can find the explaination of why this happens here: Why Partial accepts extra properties from another type?

Is an alternative to Partial to accept only fields from another type and nothing else (not requiring all the fields though)? Something like a StrictPartial?

This has been causing a lot of problems in my code base as it simply does not detect that the wrong class is being passed as parameters to the functions.

解决方案

What you really want is called exact types, where something like "Exact<Partial<A>>" would prevent excess properties in all circumstances. But TypeScript does not directly support exact types (at least not as of TS3.5) so there's no good way to represent Exact<> as a concrete type. You can simulate exact types as a generic constraint, meaning that suddenly everything that deals with them needs to become generic instead of concrete.

The only time where the type system treats types as exact is when it does excess property checks on "fresh object literals", but there are some edge cases where this doesn't happen. One of these edge cases is when your type is weak (no mandatory properties) like Partial<A>, so we can't rely on excess property checks at all.

And in a comment you said you want a class whose constructor takes an argument of type Exact<Partial<A>>. Something like

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

I'll show you how to get something like that, along with some caveats along the way.


Let's define the generic type alias

type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;

This takes a type T and a candidate type U that we want to ensure is "exactly T". It returns a new type which is like T but with extra never-valued properties corresponding to the extra properties in U. If we use this as a constraint on U, like U extends Exactly<T, U>, then we can guarantee that U matches T and has no extra properties.

For example, imagine that T is {a: string} and U is {a: string, b: number}. Then Exactly<T, U> becomes equivalent to {a: string, b: never}. Notice that U extends Exactly<T, U> is false, since their b properties are incompatible. The only way that U extends Exactly<T, U> is true is if U extends T but has no extra properties.


So we need a generic constructor, something like

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

But you can't do that because constructor functions cannot have their own type parameters inside class declarations. This is an unfortunate consequence of the interaction between generic classes and generic functions, so we will have to work around it.

Here are three ways to do it.

1: Make the class "unnecessarily generic". This makes the constructor generic as desired, but causes the concrete instances of this class to carry around a specified generic parameter:

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: Hide the constructor and use a static function instead to create instances. This static function can be generic while the class is not:

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: Make the class without the exact constraint, and give it a dummy name. Then use a type assertion to make a new class constructor from the old one which has the generic constructor signature you want:

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}

All of those should work to accept "exact" Partial<A> instances and reject things with extra properties. Well, almost.


They reject parameters with known extra properties. The type system doesn't really have a good representation for exact types, so any object can have extra properties that the compiler doesn't know about. This is the essence of substitutability of subclasses for superclasses. If I can do class X {x: string} and then class Y extends X {y: string}, then every instance of Y is also an instance of X, even though X doesn't know anything about the y property.

So you can always widen an object type to make the compiler forget about properties, and that's valid: (Excess property checking tends to make this more difficult, in some cases, but not here)

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

We know that compiles, and nothing I do can change that. And that means that even with the implementations above, you can still pass a B in:

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

The only way to prevent that is with some kind of runtime check (by examining Object.keys(smuggledOut). So it's a good idea to build such a check into your class constructor if it's really damaging to accept something with extra properties. Or, you could build your class in such a way that it will silently discard extra properties without being damaged by them. Either way, the above class definitions are about as far as the type system can be pushed in the direction of exact types, at least for now.

Hope that helps; good luck!

Link to code

这篇关于是否有 Partial 的替代方法来仅接受来自另一种类型的字段而不接受其他类型的字段?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆