13

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?

Johannes Klauß
  • 10,676
  • 16
  • 68
  • 122
  • 1
    if you have only one lvl of nested object what you need to omit tou can do something like this [playground](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgYUwFsxMEBzJOAbwFgAoORAEwC44BnGKZchp6MzQcA-OyQBXIgCM0-OJgBu5APJQhUAGqYANhOBi4kmXMZwIYGAghJdAQRjcE0iag7suPJOQDaAXXlMRx4XVG09YHcaeQFLa1sdBydQyI8nb38YuCJMJGYg6ABPJJDXVM503wCzJkVdfVF2ajgfAGtgQrSvcj92CSRWpAgAdyoAX3kJs2woYCDgNiMpWSh5HUwuAFUwfNRF4xWGKYZQSFg4GEKwPEISMkoAMWgicIa4AF5ompb2wsQqX4QdAEYikChIXpwX5wUCoPIcOAAciCyTKr0iiLgIjgKiICBgAB5bmDKG0On4ADRIixWGz2YLOMocREAPjg7GJ9yQZMKfgA3EwmEcgA) – Juraj Kocan Jun 08 '21 at 09:25

3 Answers3

7

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

hansmaad
  • 18,417
  • 9
  • 53
  • 94
4
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
3

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

distante
  • 6,438
  • 6
  • 48
  • 90
  • Hmh... is there no chance to do this via a special omit or something like this? If I need to change more deeply nested objects this will result in a lot of work. – Johannes Klauß Jun 08 '21 at 09:23
  • Not really. At least not that I know of. If you need this in a lot of places I would recommend you to use an interface for the possible `attributeValues` and change your `Campaign` to be `Campaign` where `T` is the type of `attributeValues`. It sounds like a lot of work but it is more stable and easy to maintain at the long run. – distante Jun 08 '21 at 09:31