Reputation: 11052
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
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
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
Reputation: 33111
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
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
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 }
}
}
Upvotes: 13
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;
Upvotes: 4