4

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",
    }],
};
Adam D
  • 1,962
  • 2
  • 21
  • 37

1 Answers1

3

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>

Typescript Playground with step by step explanation

BorisTB
  • 1,686
  • 1
  • 17
  • 26
  • 1
    This is perfect and exactly what I need, brilliant. It's too bad the implementation has to be so complex (you appear to be a TypeScript master), but the abstraction is spot on. – Adam D Jan 08 '22 at 14:00
  • 1
    Thank you :) I've added comments explaining every step to the playground link, hope it will help you to become "TypeScript master" as well :D – BorisTB Jan 08 '22 at 14:29
  • Really appreciate it. This might be worth publishing as a library on NPM. A lot of people could reach for these advanced "Utility Types", instead of just having `Omit` I think there could be a lot of use for your `DeepOmit, Replace, DeepReplace` etc... – Adam D Jan 08 '22 at 14:54