You can try out the following type:
// T := your object type, K := key to change, R := new key name
type PickRename<T, K extends string, R extends string> =
T extends object ? K extends keyof T ?
({ [P in R]: PickRename<T[K], K, R> } &
{ [P in Exclude<keyof T, K>]: PickRename<T[P], K, R> }) extends infer I ?
{ [PP in keyof I]: I[PP] } : never
: T
: T
Explanation using your example
If T
is an object
and key K
("_id"
) is contained, process the object, otherwise we just return T
.
Drop the old property K
(_"id"
) and add the renamed property R
("id"
); process each property recursively further.
The extends infer I ? { [PP in keyof I]: I[PP] } : never
part is just for DX/UX/formatting purposes to make the intersected type more readable.
/*
type T1 = {
id: string;
nested: {
id: string;
nested: {
id: number;
};
};
}
*/
type T1 = PickRename<Foo, "_id", "id">
To expand this to multiple properties, you can have a look at possible PickRename
implementations here.
Playground