Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to recursively Omit key from type

I want to write a type utility that omits fields recursively. Something that you would name and use like that OmitRecursively<SomeType, 'keyToOmit'>

I've tried to do it using mapped types + conditional typing but I stuck on the case when all required fields got typed correctly (hence field disappeared from nested type), but optional fields are ignored with that approach.

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

Expected behavior would be root and all nested keys that have a type with a key that should be recursively omitted is omitted. E.g

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
like image 657
Andrii Los Avatar asked Dec 23 '22 01:12

Andrii Los


1 Answers

You don't call OmitRecursevly recursively, and I also would only apply the omit recursively if the property type is an object, otherwise it should mostly work:


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 link

Edit: Updated to reflect addition of built-in Omit helper type. For older versions just define Omit.

Note Id is used mostly for cosmetic reasons (it forces the compiler to expand Pick in tooltips) and can be removed, it sometimes can cause problems in some corener cases.

Edit The original code did not work with strictNullChecks because the type of the property was type | undefined. I edited the code to distribute over unions. The conditional type OmitDistributive is used for its distributive behavior (we use it for this reason not the condition). This means that OmitRecursively will be applied to each member of the union.

Explanations

By default the Omit type does not work well on unions. Omit looks at a union as a whole and will not extract properties from each member of the union. This is mostly due to the fact that keyof will only return common properties of a union (so keyof undefined | { a: number } will actually be never, since there are no common properties).

Fortunately there is a way to drill into a union using conditional type. Conditional types will distribute over naked type parameters (see here for my explanation or the docs). In the case of OmitDistributive we don't really care about the condition (that is why we use T extends any) we just care that if we use conditional types T will in turn be each member in the union.

This means that these types are equivalent:

OmitDistributive<{ a: number, b: number} | undefined}, 'a'> = 
     OmitRecursively<{ a: number, b: number}, 'a'> | undefined 
like image 83
Titian Cernicova-Dragomir Avatar answered Dec 28 '22 15:12

Titian Cernicova-Dragomir