0

Is it possible to change the types of all matching keys in a nested interface?

I've tried a few different approaches with no luck. I'm trying to keep my type's generic signature as outlined in my examples. I want to specify the key/keys I want to change ("Target") and what types should be ("NewType"). I only want to identify the keys to change their types.

Please refer to my interface and its expected result below. My approaches for transforming the interface are below that.

It seems the second approach should work, but I don't know how to conditionally check the interface's leaf key values once the recursion ends (see conditional in the second example).

Here's a sandbox link as well.

Any help would be greatly appreciated. Thanks!

// interface 
interface og {
    a: number,
    b: number,
    c: {
        a: number,
        b: number,
        characters: {
            woman: string,
            man: string,
            elf: [boolean, string, number]
            more: {
                elf: boolean
            }
        },
    },
    elf: boolean,
};

// desired transform result (I know "dobby" isn't a typical type)
type og_transformed = {
    a: number,
    b: number,
    c: {
        a: number,
        b: number,
        characters: {
            woman: string,
            man: string,
            elf: ["dobby", "dobby", "dobby"] 
            more: {
                elf: "dobby"
            }
        },
    },
    elf: "dobby",
};

// this result could be accepted as well
type og_transformed = {
    a: number,
    b: number,
    c: {
        a: number,
        b: number,
        characters: {
            woman: string,
            man: string,
            elf: "dobby" // <- this is different
            more: {
                elf: "dobby"
            }
        },
    },
    elf: "dobby",
};

APPROACH 1:

// transforming type
type ChangeTypeOfKeys_1<Obj, Target, NewType> = {
        [K in keyof Obj]: K extends Target
            ? NewType
            : Obj[K]
    }

// type call
type dobby_1 = ChangeTypeOfKeys_1<og, 'elf', 'dobby'>

// result
type dobby_1 = {
    a: number;
    b: number;
    c: {
        a: number;
        b: number;
        characters: {
            woman: string;
            man: string;
            elf: [boolean, string, number];
            more: {
                elf: boolean;
            };
        };
    };
    elf: "dobby";
}

APPROACH 2:

// transforming type
type ChangeTypeOfKeys_2<Obj, Target, NewType> = Obj extends object 
    ? { 
        [K in keyof Obj]: ChangeTypeOfKeys_2<Obj[K], Target, NewType>
    }
    : NewType // <- how do I check the key's value in this conditional ???

// type call
type dobby_2 = ChangeTypeOfKeys_2<og, 'elf', 'dobby'>

// result
type dobby_2 = {
    a: "dobby";
    b: "dobby";
    c: {
        a: "dobby";
        b: "dobby";
        characters: {
            woman: "dobby";
            man: "dobby";
            elf: ["dobby", "dobby", "dobby"];
            more: {
                elf: "dobby";
            };
        };
    };
    elf: "dobby";
}

SOLUTION (from Alex below):

Mainly here to show the on hover reference in the comments below and save anyone from overlooking something so simple, such as myself...

I've also added a solution sandbox with extended types and a description of what each does. The example interface has been modified for clarity.

Solution sandbox

// transforming type
type ChangeTypeOfKeys_3<Obj, Target, NewType> = {
    [K in keyof Obj]: K extends Target 
        ? NewType
        : Obj[K] extends object 
            ? ChangeTypeOfKeys_3<Obj[K], Target, NewType>
            : Obj[K]
}

// type call
type dobby_3 = ChangeTypeOfKeys_3<og, 'elf', 'dobby'>

// result (on hover)
type dobby_3 = {
    a: number;
    b: number;
    c: ChangeTypeOfKeys_3<{
        a: number;
        b: number;
        characters: {
            woman: string;
            man: string;
            elf: [boolean, string, number];
            more: {
                elf: boolean;
            };
        };
    }, "elf", "dobby">;
    elf: "dobby";
}

1 Answer 1

2

You definately need it to be recursive to drill into deeply nested objects. But there are actually two conditional tests you need to do:

  1. Is this a key that should be replaced? You are already doing this.
  2. Is this value an object that needs to be recursed into? If so, recurse, else we just return its type unaltered.

Your two attempts each do half of this. You need to combine them into a nested conditional type that does both checks.

type ChangeTypeOfKeys<Obj, Target, NewType> = {
    [K in keyof Obj]:

        // does this value have the target key? If so replace it.
        K extends Target ? NewType 

        // Else, is this value an object? If so, recursively change call ChangeTypeOfKeys
        : Obj[K] extends object ? ChangeTypeOfKeys<Obj[K], Target, NewType>

        // Else, use the type of this leaf without modification.
        : Obj[K]
}

type OgTransformed = ChangeTypeOfKeys<Og, 'elf', 'dobby'>
declare const elfed: OgTransformed
const elfName = elfed.c.characters.elf // type: 'dobby'
const elfMore = elfed.c.characters.more.elf // type: 'dobby'

Playground

Sign up to request clarification or add additional context in comments.

4 Comments

Ahh. Thank you very much for the concise reply. It works perfectly.
From your answer, I realized I was actually setting the transformer type correctly in other iterations. Unfortunately, I didn't realize that on hover TS doesn't display types all the way down the interface. It simply displays the next recursion of the transformer type. I wasn't aware you actually had to access the types declaratively as you did at the end of your answer. TIL.
I've included what I'm talking about on hover in my original post for anyone who may read this.
Yeah type script doesn't always fully process types in tooltips. Note how the inner types are wrapped in ChangeTypeOfKeys<{ ... }>, which indicates the input type being transformed and the type alias doing the transformation. Getting the real type from a nested value of that requires drilling into it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.