Typescript:如何从联合类型中排除仅具有可选属性的接口

时间:2019-07-24 14:26:29

标签: typescript

我有一些类似于这些的接口,以后要排除的接口仅包含可选属性:

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

如果Exclude<T, U> = T extends U ? never : T仅由可选属性组成,我可以看到A extends B显然是trueB。为什么会这样?

又如何从接口的联合类型中适当排除类型?

1 个答案:

答案 0 :(得分:2)

给出以下类型:

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<A | B, B>会同时删除AB来自工会,您将剩下never


当然,将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中无法将“ 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<T>的对象类型为T,并要求所有可选类型,但它们的属性类型包括undefinedUndefToOpt<T>的对象类型为T,并将所有类型为undefined的属性都设为可选属性。这些差不多是彼此的逆运算(只要您没有必需的类型,包括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