Deep modification v4
I am currently building a more robust solution in my gist that better handles arrays and allows to remove a key or modify its ?
optionality.
interface Original {
x: {
a: string
b: string
}
}
interface Overrides {
x: {
a: never // <- this key will be deleted
b?: string // <- this will become optional
}
}
/* result = {
x: {
b?: string
}
} */
Deep modification v3
*note, version 2 is in the history of this answer.
interface Original {
a: {
a: string
b: { a: string }
c: string
d: string // <- keep this one
}
}
interface Overrides {
a: {
a: { a: number } // <- overwrite string with object
b: number // <- overwrite object with number
c: number // <- overwrite string with number
e: number // <- new property
}
}
type ModifiedType = ModifyDeep<Original, Overrides>
interface ModifiedInterface extends ModifyDeep<Original, Overrides> {}
Result
const example: ModifiedType = {
a: {
a: { a: number },
b: number,
c: number,
d: string,
e: number,
}
}
The code
type ModifyDeep<A, B extends DeepPartialAny<A>> = {
[K in keyof A | keyof B]: // For all keys in A and B:
K extends keyof A // ββββ
? K extends keyof B // ββββΌβ key K exists in both A and B
? A[K] extends AnyObject // β β΄βββ
? B[K] extends AnyObject // β ββββΌβ both A and B are objects
? B[K] extends AnyFunction // β β ββ Avoid deeply modifying functions which results in {}
? B[K] // β β β
: ModifyDeep<A[K], B[K]> // β β ββββ We need to go deeper (recursively)
: B[K] // β ββ B is a primitive π use B as the final type (new type)
: B[K] // β ββ A is a primitive π use B as the final type (new type)
: A[K] // ββ key only exists in A π use A as the final type (original type)
: B[K] // ββ key only exists in B π use B as the final type (new type)
}
type AnyObject = Record<string, any>
type AnyFunction = (...args: any[]) => any
// This type is here only for some intellisense for the overrides object
type DeepPartialAny<T> = {
/** Makes each property optional and turns each leaf property into any, allowing for type overrides by narrowing any. */
[P in keyof T]?: T[P] extends AnyObject ? DeepPartialAny<T[P]> : any
}
*Note, type DeepPartialAny
is there just for type hints, but it's not perfect. Technically, the logic of the ModifyDeep
Β type allows to replace leaf nodes {a: string}
with objects {a: {b: ... }}
and vice versa, but DeepPartialAny
will complain when overriding an object
with a flat primitive with an error such as this
Type 'number' has no properties in common with type 'DeepPartialAny<{ a: string; }>'
However, you can safely ignore the error (with /// @ts-ignore
or remove extends DeepPartialAny
constraint altogether. The resulting type is computed correctly anyway.
example
TypeScript Playground
type ModifyDeep<A, B extends DeepPartialAny<A>> = {
[K in keyof A | keyof B]:
K extends keyof A
? K extends keyof B
? A[K] extends AnyObject
? B[K] extends AnyObject
? B[K] extends AnyFunction
? B[K]
: ModifyDeep<A[K], B[K]>
: B[K]
: B[K]
: A[K]
: B[K]
}
type AnyObject = Record<string, any>
type AnyFunction = (...args: any[]) => any
type DeepPartialAny<T> = {
/** Makes each property optional and turns each leaf property into any, allowing for type overrides by narrowing any. */
[P in keyof T]?: T[P] extends AnyObject ? DeepPartialAny<T[P]> : any
}
interface Original {
a: {
a: string
b: { a: string }
c: { a: string }
}
b: string
c: { a: string }
}
interface Overrides {
a: {
a: { a: number } // <- overwrite string with object
b: number // <- overwrite object with number
c: { b: number } // <- add new child property
d: number // <- new primitive property
}
d: { a: number } // <- new object property
}
//@ts-ignore // overriding an object with a flat value raises an error although the resulting type is calculated correctly
type ModifiedType = ModifyDeep<Original, Overrides>
//@ts-ignore
interface ModifiedInterface extends ModifyDeep<Original, Overrides> {}
// Try modifying the properties here to prove that the type is working
const t: ModifiedType = {
a: {
a: { a: 0 },
b: 0,
c: { a: '', b: 0},
d: 0,
},
b: '',
c: { a: '' },
d: { a: 0 },
}