I am struggling to make a generic which would recursively modify all elements found in a structure of nested, recursive data. Here is an example of my data structure. Any post could have an infinite number of comments with replies using this recursive data definition.
type Post = {
user: string,
content: string,
comments: Comment[],
reactions: Reaction[],
}
type Comment = {
user: string,
content: string,
replies: Comment[],
}
type Reaction = {
user: string,
reaction: "laugh" | "cry" | "smile",
}
What I would like is a generic wrapper that I could use with these and any other data types that would replace every user field with something else. I could do this for the top level:
type UserFilled<T> = Omit<"user", T> & { user: { id: string, name: string }}
But this would only change the user field for the Post. I would also like it to crawl down and replace the change the user field for each of the comments, and if there were more fields, for the reactions, likes, or any other structures with a user in them.
I've seen this answer about omitting something recursively but I was not able to add the modified property back in using a union and I'm wondering if there's a more straightforward way to do this while not just omitting but also replacing the field?
For example, using the generic I would like to be able to do this:
const post: Post = {
user: "1234",
content: "this is a post",
comments: [{
user: "3456",
content: "I agree",
replies: [{
user: "1234",
content: "thanks",
}],
}],
reactions: [{
user: "5678",
reaction: "smile",
}],
};
const postWUserInfo: UserFilled<Post> = {
user: { id: "1234", name: "Bob" },
content: "this is a post",
comments: [{
user: { id: "3456", name: "Jim" },
content: "I agree",
replies: [{
user: { id: "1234", name: "Bob" },
content: "thanks",
}],
}],
reactions: [{
user: { id: "5678", name: "Jim" },
reaction: "smile",
}],
};
CodePudding user response:
You can create a DeepReplace utility that would recursively check and replace keys. Also I'd strongly suggest to only replace value and make sure the key will stay same.
// "not object"
type Primitive = string | Function | number | boolean | Symbol | undefined | null
// If T has key K ("user"), replace it
type ReplaceKey<T, K extends string, R> = T extends Record<K, unknown> ? Omit<T, K> & Record<K, R> : T
// Check and replace object values
type DeepReplaceHelper<T, K extends string, R, ReplacedT = ReplaceKey<T, K, R>> = {
[Key in keyof ReplacedT]: ReplacedT[Key] extends Primitive ? ReplacedT[Key] : ReplacedT[Key] extends unknown[] ? DeepReplace<ReplacedT[Key][number], K, R>[] : DeepReplace<ReplacedT[Key], K, R>
}
// T = object, K = key to replace, R = replacement value
type DeepReplace<T, K extends string, R> = T extends Primitive ? T : DeepReplaceHelper<T, K, R>
// Define new type for "user" key
interface UserReplacement {
id: string
name: string
}
type UserFilled<T> = DeepReplace<T, "user", UserReplacement>
