Johannes Klauß
Johannes Klauß

Reputation: 11052

Typescript: Omit nested property

I have an interface like this:

export interface Campaign {
  id: string
  orders?: number
  avgOrderValue?: number
  optionalAttributes: string[]
  attributeValues: {
    optionalAttributes: CampaignAttribute[]
    mandatoryAttributes: CampaignAttribute[]
    values?: { [key: string]: unknown }
  }
  created: number
  lastUpdated: number
}

And I want to create a type out of this for my form that needs to omit the attributeValues.optionalAttributes and attributeValues.mandatoryAttributes from the interface.

I was thinking that maybe Typescript can do something like this:

export type CampaignFormValues = Omit<Campaign, 'attributeValues.mandatoryAttributes'>

But this doesn't work.

I used the answer from this question: Deep Omit with typescript But this answer just deep omits every matched key, so using it like this:

export type CampaignFormValues = Omit<Campaign, 'optionalAttributes'>

Would also remove the root level optionalAttributes which I want to keep.

Is there any way to do a nested omit with Typescript?

Upvotes: 24

Views: 22019

Answers (5)

Landei7
Landei7

Reputation: 1

Piggybacking on Pau Ibáñez' answer (huge props), I created a not very simple utility type that supports autocomplete and arrays in object properties. I hope my explanations are intelligible.

/**
 * Union type of all paths (string) to every nested type property
 * @template Object - The object for which the paths should be determined
 * @param {Object} T
 */
type Paths<T> =
T extends Array<infer ArrayContentType>
    ? Paths<ArrayContentType> // if passed type is an array, recursively call Paths with the array content type
    : {
          [K in keyof T]-?: T extends object // escape optional properties that would otherwise add undefined to the paths
              ? `${Exclude<K, symbol>}${`.${Paths<T[K]>}` | ''}` // `<PropKey>.<recursively call for every nested object property>`
              : `${Exclude<K, symbol>}`; // if no object, return prop key as string. Exclude symbol from possible prop key types. They don't work in the string
      }[keyof T]; // return the one object property, not the whole object



/**
 * Utility type that omits a certain property nested with in an object specified by a path.
 * Provides autocomplete for every possible property paths.
 * @template Schema - The object the property should be omitted from
 * @template Path - Dot-separated string of keys to the nested property to be omitted
 * @param {Schema} Object
 * @param {Path} Path
 */
type NestedOmit<
    Schema,
    Path extends Paths<Schema>,
> = Path extends `${infer Head}.${infer Tail}`
    ? Head extends keyof Schema
        ? {
              [Key in keyof Schema]: Schema[Key] extends infer PropType
                  ? Key extends Head
                      ? PropType extends Array<infer ArrayContentType>
                          ? Tail extends Paths<ArrayContentType>
                              ? Array<NestedOmit<ArrayContentType, Tail>>
                              : PropType
                          : PropType extends object
                            ? Tail extends Paths<PropType>
                                ? NestedOmit<PropType, Tail>
                                : PropType
                            : PropType
                      : PropType
                  : never;
          }
        : Schema
    : Omit<Schema, Path>;

// EXPLANATION: (ordered by line of type declaration body)
// checks that Path is a dot separated string and returns the part before the first dot as type alias Head, the rest as Tail
// checks if Head matches a key of the passed type "Schema"

// alias the current property type
// checks if the passed object's key is the one specified at the start in paths
// checks if the current prop type is an array, look at the array content type
// checks if Tail is a valid path to a property in the array content type
// recursive call, pass the substring after the first .
// in case of invalid path in Tail, return the property

// -> analogous for objects


// neither array, nor object -> return type property
// our current prop isn't the one specified in the path -> return it
// type inferring shouldn't break

// in case of invalid path, return the whole passed type
// here we can't infer a Tail anymore -> we are at the end of the dot separated path
// DONE!

Upvotes: 0

Pau Ib&#225;&#241;ez
Pau Ib&#225;&#241;ez

Reputation: 154

I know I'm late to the discussion, but I've created a very simple Utility type that acomplishes the NestedOmit with . separated keys qauite simply;

  type NestedOmit<
  Schema,
  Path extends string,
> = Path extends `${infer Head}.${infer Tail}`
  ? Head extends keyof Schema
    ? {
        [K in keyof Schema]: K extends Head
          ? NestedOmit<Schema[K], Tail>
          : Schema[K];
      }
    : Schema
  : Omit<Schema, Path>;

I hope this is usefull for someone!

Upvotes: 5

type A = {
    a: {
        b: string
        c: string
    }
    x: {
        y: number
        z: number,
        w: {
            u: number
        }
    }
}
type Primitives = string | number | boolean | symbol

/**
 * Get all valid nested pathes of object
 */
type AllProps<Obj, Cache extends Array<Primitives> = []> =
    Obj extends Primitives ? Cache : {
        [Prop in keyof Obj]:
        | [...Cache, Prop] // <------ it should be unionized with recursion call
        | AllProps<Obj[Prop], [...Cache, Prop]>
    }[keyof Obj]

type Head<T extends ReadonlyArray<any>> =
    T extends []
    ? never
    : T extends [infer Head]
    ? Head
    : T extends [infer Head, ...infer _]
    ? Head
    : never


type Tail<T extends ReadonlyArray<any>> =
    T extends []
    ? []
    : T extends [infer _]
    ? []
    : T extends [infer _, ...infer Rest]
    ? Rest
    : never

type Last<T extends ReadonlyArray<any>> = T['length'] extends 1 ? true : false


type OmitBase<Obj, Path extends ReadonlyArray<any>> =
    Last<Path> extends true
    ? {
        [Prop in Exclude<keyof Obj, Head<Path>>]: Obj[Prop]
    } : {
        [Prop in keyof Obj]: OmitBase<Obj[Prop], Tail<Path>>
    }

// we should allow only existing properties in right order
type OmitBy<Obj, Keys extends AllProps<Obj>> = OmitBase<A, Keys>

type Result = OmitBy<A,['a', 'b']> // ok

type Result2 = OmitBy<A,['b']> // expected error. order should be preserved


Playground

More explanation you can find in my blog

Above solution works with deep nested types

If you want to use dot syntax prop1.prop2, consider next type:

type Split<Str, Cache extends string[] = []> =
    Str extends `${infer Method}.${infer Rest}`
    ? Split<Rest, [...Cache, Method]>
    : Str extends `${infer Last}`
    ? [...Cache, Last,]
    : never
    
type WithDots = OmitBy<A, Split<'a.b'>> // ok

Upvotes: 6

hansmaad
hansmaad

Reputation: 18915

First, Omit attributeValues, then add it back with properties removed.

export interface Campaign {
  id: string
  attributeValues: {
    optionalAttributes: string[]
    mandatoryAttributes: string[]
    values?: { [key: string]: unknown }
  }
}

type ChangeFields<T, R> = Omit<T, keyof R> & R;
type CampaignForm = ChangeFields<Campaign, { 
  attributeValues: Omit<Campaign['attributeValues'], 'mandatoryAttributes'|'optionalAttributes'> 
}>;

const form: CampaignForm = {
  id: '123',
  attributeValues: {
    values: { '1': 1 }
  }
}

Playground

Upvotes: 13

distante
distante

Reputation: 7025

You need to create a new interface where attributeValues is overwritten:

export interface Campaign {
  id: string
  orders?: number
  avgOrderValue?: number
  optionalAttributes: string[]
  attributeValues: {
    optionalAttributes: CampaignAttribute[]
    mandatoryAttributes: CampaignAttribute[]
    values?: { [key: string]: unknown }
  }
  created: number
  lastUpdated: number
}

interface MyOtherCampaign extends Omit<Campaign, 'attributeValues'> {
    attributeValues: {
      values?: { [key: string]: unknown }
    }
}

let x:MyOtherCampaign;

enter image description here

Playground

Upvotes: 4

Related Questions