如何从联合类型中删除较宽的类型而不在TypeScript中删除其子类型?

Mih*_*dis 6 typescript typescript2.8

使用“排除”运算符不起作用。

type test = Exclude<'a'|'b'|string, string>
// produces type test = never
Run Code Online (Sandbox Code Playgroud)

我可以理解为什么“除了字符串”也意味着排除所有字符串文字,但是我如何才能获得'a'|'b'out 'a'|'b'|string呢?

如果需要,假定使用最新的TypeScript。

用例如下:

假设第三方库定义了这种类型:

export interface JSONSchema4 {
  id?: string
  $ref?: string
  $schema?: string
  title?: string
  description?: string
  default?: JSONSchema4Type
  multipleOf?: number
  maximum?: number
  exclusiveMaximum?: boolean
  minimum?: number
  exclusiveMinimum?: boolean
  maxLength?: number
  minLength?: number
  pattern?: string
  // to allow third party extensions
  [k: string]: any
}
Run Code Online (Sandbox Code Playgroud)

现在,我想做的是获得KNOWN属性的并集:

type KnownProperties = Exclude<keyof JSONSchema4, string|number>
Run Code Online (Sandbox Code Playgroud)

可以理解的是,这失败了并且给出了一个空类型。

如果您正在阅读本文,但是我被公交车撞到了,可以在GitHub线程中找到答案。

Mih*_*dis 12

我在这个GitHub线程中@ferdaber得到了一个解决方案。

编辑:事实证明,这是由@ajafff于1986年发布的

该解决方案需要TypeScript 2.8的条件类型,并且如下所示:

type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
Run Code Online (Sandbox Code Playgroud)

以下是我的解释尝试:

该解决方案基于一个事实,即stringextends string(与'a'extends一样string),但string不扩展'a',对于数字也是如此。基本上,我们必须将其extends视为“进入”

首先,它创建一个映射类型,其中对于T的每个键,其值为:

  • 如果string扩展键(键是字符串,不是子类型)=>从不
  • 如果数字扩展键(键是数字,而不是子类型)=>从不
  • 否则,实际的字符串键

然后,它实际上执行了valueof以获得所有值的并集:

type ValuesOf<T> = T extends { [_ in keyof T]: infer U } ? U : never
Run Code Online (Sandbox Code Playgroud)

或者,更确切地说:

interface test {
  req: string
  opt: string
  [k: string]: any
}
type FirstHalf<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
}

type ValuesOf<T> = T extends { [_ in keyof T]: infer U } ? U : never
// or equivalently, since T here, and T in FirstHalf have the same keys,
// we can use T from FirstHalf instead:
type SecondHalf<First, T> = First extends { [_ in keyof T]: infer U } ? U : never;

type a = FirstHalf<test>
//Output:
type a = {
    [x: string]: never;
    req: "req";
    opt?: "opt" | undefined;
}
type a2 = ValuesOf<a> //  "req" | "opt" // Success!
type a2b = SecondHalf<a, test> //  "req" | "opt" // Success!

// Substituting, to create a single type definition, we get @ferdaber's solution:
type KnownKeys<T> = {
  [K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
// type b = KnownKeys<test> //  "req" | "opt" // Absolutely glorious!
Run Code Online (Sandbox Code Playgroud)

如果有人在GitHub上提出异议,则在GitHub线程中进行解释

  • 哇!那很聪明。 (3认同)
  • 答案太棒了!!我认为接口 test.opt 应该是可选的,因为类型 a 是从 FirstHalf&lt;T&gt; 生成的,并且 a.opt 的值为“undefined” (2认同)