如何递归地从类型中省略键。

7

我希望编写一个递归省略字段的类型工具。你可以像这样命名和使用它:OmitRecursively<SomeType, 'keyToOmit'>

我尝试使用映射类型+条件类型来实现,但是当所有必需字段都被正确地输入类型(因此从嵌套类型中消失)时,我卡在了可选字段被忽略的情况。

// This is for one function that removes recursively __typename field 
// that Appolo client adds
type Deapolify<T extends { __typename: string }> = Omit<
  { [P in keyof T]: T[P] extends { __typename: string } ? Deapolify<T[P]> : T[P] },
  '__typename'
>

// Or more generic attempt

type OmitRecursively<T extends any, K extends keyof T> = Omit<
  { [P in keyof T]: T[P] extends any ? Omit<T[P], K> : never },
  K
>

期望的行为是忽略根和所有嵌套键,这些键具有应该被递归省略的键类型。例如:

type A = {
  keyToKeep: string
  keyToOmit: string
  nested: {
    keyToKeep: string
    keyToOmit: string
  }
  nestedOptional?: {
    keyToKeep: string
    keyToOmit: string
  }
}

type Result = OmitRecursively<A, 'keyToOmit'>

type Expected = {
  keyToKeep: string
  nested: {
    keyToKeep: string
  }
  nestedOptional?: {
    keyToKeep: string
  }
} 

Expected === Result
2个回答

18
你不要递归调用 OmitRecursevly,如果属性类型是对象,我也只会递归应用省略,否则它应该大部分正常工作。

type OmitDistributive<T, K extends PropertyKey> = T extends any ? (T extends object ? Id<OmitRecursively<T, K>> : T) : never;
type Id<T> = {} & { [P in keyof T] : T[P]} // Cosmetic use only makes the tooltips expad the type can be removed 
type OmitRecursively<T, K extends PropertyKey> = Omit<
    { [P in keyof T]: OmitDistributive<T[P], K> },
    K
>

type A = {
    keyToKeep: string
    keyToOmit: string
    nested: {
        keyToKeep: string
        keyToOmit: string
    }
    nestedOptional?: {
        keyToKeep: string
        keyToOmit: string
    }
}

type Result = OmitRecursively<A, 'keyToOmit'>

游乐场链接

编辑:更新以反映内置的Omit辅助类型的添加。对于旧版本,只需定义Omit。

注意:Id主要用于装饰目的(它强制编译器在工具提示中展开Pick),可以删除它,但在某些边缘情况下,它有时会引起问题。

编辑:原始代码在strictNullChecks下不起作用,因为属性的类型是type | undefined。我修改了代码以在联合类型上进行分发。条件类型OmitDistributive用于其分发行为(我们之所以使用它是因为这个原因,而不是条件)。这意味着OmitRecursively将应用于联合的每个成员。

解释

默认情况下,Omit 类型在联合类型上的工作效果不佳。 Omit 将联合类型视为一个整体,并不会从联合类型的每个成员中提取属性。这主要是因为 keyof 只会返回联合类型的公共属性(所以 keyof undefined | { a: number } 实际上是 never,因为没有公共属性)。
幸运的是,我们可以使用条件类型来深入到联合类型中。条件类型会在裸类型参数上进行分布(请参阅这里以获取我的解释或文档)。在 OmitDistributive 的情况下,我们并不真正关心条件(这就是为什么我们使用 T extends any),我们只关心如果我们使用条件类型,T 将成为联合类型的每个成员。
这意味着这些类型是等价的:
OmitDistributive<{ a: number, b: number} | undefined}, 'a'> = 
     OmitRecursively<{ a: number, b: number}, 'a'> | undefined 

它不起作用。 我刚刚检查了那段代码。它可能与编译器配置有关吗? - Andrii Los
@AndriiLos 工作正常,严格的空值检查会导致问题,在一秒钟内修复。 - Titian Cernicova-Dragomir
太棒了,它能正常工作!我的问题是,你能否逐步详细解释一下它的原理?因为我对OmitDistributive和其他内容完全不了解 :) 另外,为什么你使用了不同的Omit而不是TS内置的原始版本? - Andrii Los
1
@AndriiLos编辑了答案,希望现在更加清晰明了。 - Titian Cernicova-Dragomir
如何消除在播放链接第7行的类型错误? 类型'K'不满足约束条件'string | number | symbol'。 - Rocaboca
显示剩余5条评论

1
我写了一篇关于这个主题的扩展文章:在TypeScript中编写递归实用类型
首先,是代码:
type UnionOmit<T, K extends string | number | symbol> = T extends unknown
  ? Omit<T, K>
  : never;
type NullUnionOmit<T, K extends string | number | symbol> = null extends T
  ? UnionOmit<NonNullable<T>, K>
  : UnionOmit<T, K>;
type RecursiveOmitHelper<T, K extends string | number | symbol> = {
  [P in keyof T]: RecursiveOmit<T[P], K>;
};
type RecursiveOmit<T, K extends string | number | symbol> = T extends {
  [P in K]: any;
}
  ? NullUnionOmit<RecursiveOmitHelper<T, K>, K>
  : RecursiveOmitHelper<T, K>;

const cleanSolarSystem: RecursiveOmit<SolarSystem, "__typename"> = {
  //__typename: "SolarSystem",
  id: 123,
  name: "The Solar System",
  star: {
    //__typename: "Planet",
    id: 123,
    inhabitants: null,
    name: "Sun",
    size: 9999,
  },
  planets: [
    {
      //__typename: "Planet",
      id: 123,
      name: "Earth",
      size: 12345,
      inhabitants: [
        {
          //__typename: "LifeForm",
          id: 123,
          name: "Human",
        },
      ],
    },
  ],
};

...以及游乐场链接

所有这些都是为了涵盖以下情况:
  • 联合
  • 可空类型(看起来像联合,但它们是“粘性的”)
  • 函数和任何其他不适合映射的类型。(函数失去其可调用签名)

在我看来,这与其他答案基本相同,但它的方法稍微更加简洁。


这段代码存在一个错误,其值类似于:Record<string, any> | null - sbdchd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接