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

qij5mzcb  于 2023-02-05  发布在  TypeScript
关注(0)|答案(2)|浏览(119)

我想写一个递归地省略字段的类型实用程序,你可以这样命名和使用
我尝试过使用Map类型+条件类型来实现,但我坚持这样一种情况,即所有必需字段都正确键入(因此字段从嵌套类型中消失),但可选字段在这种方法中被忽略。

// 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
rxztt3cl

rxztt3cl1#

你不能递归地调用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 extends any, 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'>

Playground链接

    • 编辑**:更新以反映内置Omit帮助器类型的添加。对于旧版本,只需定义"忽略"。
    • 注**Id主要用于装饰(它强制编译器在工具提示中展开Pick),可以删除,它有时会在某些核心情况下导致问题。
    • 编辑**原始代码不适用于strictNullChecks,因为属性的类型是type | undefined。我编辑了代码以在联合体上分发。条件类型OmitDistributive用于其分发行为(我们使用它的原因不是条件)。这意味着OmitRecursively将应用于联合体的每个成员。
    • 解释**

默认情况下Omit类型在联合体上不起作用。Omit将联合体视为一个整体,不会从联合体的每个成员中提取属性。这主要是由于keyof只返回联合体的公共属性(因此keyof undefined | { a: number }实际上是never,因为没有公共属性)。
幸运的是,有一种方法可以使用条件类型钻取联合。条件类型将分布在裸类型参数上(关于docs的解释,请参见此处)。对于OmitDistributive,我们并不真正关心条件(这就是为什么我们使用T extends any)我们只关心如果我们使用条件类型,T将依次是联合体中的每个成员。
这意味着这些类型是等效的:

OmitDistributive<{ a: number, b: number} | undefined}, 'a'> = 
     OmitRecursively<{ a: number, b: number}, 'a'> | undefined
pkwftd7m

pkwftd7m2#

我写了一篇关于这个主题的扩展文章:Writing a Recursive Utility Type in 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",
        },
      ],
    },
  ],
};

......还有操场链接
所有这些都需要涵盖以下情况:

  • 工会
  • 可空类型(看起来像联合,但它们是"粘性"的)
  • Map时行为不佳的函数和任何其他类型。(函数丢失其可调用签名)

这在很大程度上等同于另一个答案,但在我看来,它有一个稍微干净的方法。

相关问题