【问题标题】:Typescript: how to Exclude an interface with only optional properties from a union type打字稿:如何从联合类型中排除仅具有可选属性的接口
【发布时间】:2019-12-02 17:55:05
【问题描述】:

我有一些与这些类似的接口,其中我想稍后排除的接口仅包含可选属性:

interface P {
    id: string
}

interface A extends P {
    attrA: string
}

interface B extends P {
    attrB?: string
}

现在从与其他接口的联合中排除此接口不起作用:

type R = Exclude<A | B, B> // expected: A, is: never

类型Rnever,但应该是A

如果B 仅包含可选属性,我可以看到Exclude&lt;T, U&gt; = T extends U ? never : TA extends B 显然是true。这是为什么呢?

我怎样才能正确地从接口的联合类型中排除一个类型?

【问题讨论】:

  • PAB 是同一类型。 TypeScript 的类型系统是structural,不是名义上的。那就是:名称不定义类型,形状定义。 Exclude&lt;A | B, B&gt;Exclude&lt;A | A, A&gt;Exclude&lt;A, A&gt;never 相同。
  • 谢谢,这已经让它更清楚了,但我的实际类型不一样,它仍然显示为never。我会尝试用更好的类型更新示例。
  • 是的,A extends B 被认为是真的,因为 A 类型的对象几乎总是可以分配给 B 类型的变量,除了某些东西像{id: "a", attrA: "a", attrB: 123},它需要减法/否定类型才能更合理地表示。你能确定B 有一个与A 不同的属性吗?

标签: typescript


【解决方案1】:

鉴于这些类型:

interface P {
    id: string
}

interface A extends P {
    attrA: string
}

interface B extends P {
    attrB?: string
}

type R = Exclude<A | B, B>; // never

R 类型为 never,因为编译器认为 A extends B 为真:

const a: A = { id: "a", attrA: "A" };
const b: B = a; // okay

您可以看到a 是一个有效的A,但它也是一个有效的B。 TypeScript 中的对象类型不是exact;您可以通过添加属性来扩展类型(这就是为什么A 可以分配给P,即使A 有一个额外的属性)。从类型系统的角度来看,A 类型的每个值也是B 类型的值,因此,Exclude&lt;A | B, B&gt; 从联合中删除了AB,剩下的是@987654341 @。


当然,将A 分配给B 实际上不是类型安全的。编译器假定A 类型的每个 值也是B 类型,但实际上它更像几乎 每个值。 A 类型的某些特定值不应分配给 B 类型,即具有不兼容类型的 attrB 属性的任何值:

const aButNotB = {id: "a", attrA: "A", attrB: 123}
const alsoA: A = aButNotB; // okay
const notB: B = aButNotB; // error, attrB is incompatible
const butWait: B = alsoA; // no error?! attrB is still incompatible, but compiler forgot!

但由于 TypeScript 尚不支持 negated types,因此 TypeScript 无法将“Every A which is not a B”表示为具体类型。因此,在比较两种对象类型时,编译器只会忽略其中一个对象中存在的可选属性......导致了这个盲点。


所以这就是它发生的原因。至于如何解决这个问题,这取决于您的用例。

理想情况下,如果您真的需要能够区分类型联合的值,您可以使用discriminated union。这意味着联合应该包含一些可以用来绝对区分它们的属性或属性。例如,让我们假设添加一个type 判别属性:

interface Aʹ extends A {
  type: "A";
}

interface Bʹ extends B {
  type: "B";
}

type Rʹ = Exclude<Aʹ | Bʹ, Bʹ>; // Aʹ

现在无法将 类型的值分配给 类型,反之亦然,现在 Exclude 的行为符合您的预期。


或者,如果您只是在查看如何按照您期望的方式执行 Exclude,您可以在处理它们之前修改您的 AB 类型,然后取消修改结果,如下所示:

type OptToUndef<T> = {
  [K in keyof Required<T>]: T[K] | ({} extends Pick<T, K> ? undefined : never)
};
type UndefToOpt<T> = (Partial<
  Pick<T, { [K in keyof T]: undefined extends T[K] ? K : never }[keyof T]>
> &
  Pick<
    T,
    { [K in keyof T]: undefined extends T[K] ? never : K }[keyof T]
  >) extends infer O
  ? { [K in keyof O]: O[K] }
  : never;

基本上OptToUndef&lt;T&gt; 采用对象类型T 并要求所有可选类型,但它们的属性类型包括undefined。并且UndefToOpt&lt;T&gt; 采用对象类型T 并将所有类型包括undefined 的属性变为可选属性。这些或多或少是彼此的逆运算(只要您没有 required 类型,包括undefined)。然后你可以这样做:

type UA = OptToUndef<A>; // {id: string; attrA: string }
type UB = OptToUndef<B>; // {id: string; attrB: string | undefined }
type UR = Exclude<UA | UB, UB>; // same as UA
type Rfixed = UndefToOpt<UR>; // same as A

类似的方法可能对您有用,您可以将 B 调整为不是全部可选的,然后在完成后取消调整。

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

Link to code

【讨论】:

  • 非常感谢您提供冗长而完整的解释。
猜你喜欢
  • 2019-06-30
  • 2021-02-25
  • 2019-02-25
  • 2020-09-14
  • 2022-01-04
  • 2019-08-15
  • 2017-09-16
  • 2021-08-13
  • 2022-10-15
相关资源
最近更新 更多