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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With