The following type removes desired properties from all nested depths of the input type:
type DeepRemoveProps<T, P extends string> = T extends object
? { [K in Exclude<keyof T, P>]: DeepRemoveProps<T[K], P> }
: T;
For example:
type Input = {
a: {
b: string,
c: {
xx: 'removed',
d: string
}
},
yy: { removed: true },
xx: 'im removed'
};
type Result = DeepRemoveProps<Input, 'xx' | 'yy'>;
In this case Result
resolves to:
type Result = {
a: {
b: string,
c: {
d: string
}
}
};
But this type fails to preserve whether fields are optional!
For example, with this type:
type Input = {
a: {
b?: string,
c: {
xx: 'removed',
d?: string
}
},
yy: { removed: true },
xx: 'im removed'
};
the result of DeepRemoveProps<Input, 'xx' | 'yy'>
is exactly the same as above, with all properties required - for some reason, Input['a']['b']
and Input['a']['c']['d']
are not having their ?
preserved. I was under the impression that the mapped type used in DeepRemoveProps
would by default preserve whether or not a ?
is present.
How can I implement DeepRemoveProps
in such a way that it preserves whether fields are optional?
If you want a mapped type to preserve the optional and readonly
modifiers for properties you're mapping over, you need to write a homomorphic mapped type (see What does "homomorphic mapped type" mean?) of the form {[⋯ in keyof T]: ⋯}
for generic T
, where in keyof
appears directly. You have in Exclude<keyof
which breaks it. (TypeScript needs to know what the original type is, in order to copy property modifiers. If it sees in keyof T
it will understand you're copying from T
. But if the key set after in
is some arbitrary type that merely depends on keyof T
somewhere, it can't be sure.)
My suggestion would be to use key remapping with as
and filter out keys in the as
clause instead of the in
clause. That is, use Exclude
to act on K
and not keyof T
:
type DeepRemoveProps<T, P extends string> = T extends object ? {
[K in keyof T as Exclude<K, P>]: DeepRemoveProps<T[K], P>
} : T;
This maps any key K
that overlaps with P
to never
, which has the effect of suppressing the key from the output. Now your code should behave as expected:
type Result = DeepRemoveProps<Input, 'xx' | 'yy'>;
/* type Result = {
a: {
b?: string | undefined;
c: {
d?: string | undefined;
};
};
} */