Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Omit nested property

Tags:

typescript

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?

like image 727
Johannes Klauß Avatar asked Jun 08 '21 09:06

Johannes Klauß


2 Answers

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
like image 75
captain-yossarian Avatar answered Sep 29 '22 10:09

captain-yossarian


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

like image 39
distante Avatar answered Sep 29 '22 11:09

distante