5

I have a type

type Foo = {
  a: {
    b: {
      c: string[]
      ...rest
    }
    ...rest
  }
  ...rest
}

How can i change a.b.c to another type but keep the rest of the properties?

Asaf Aviv
  • 11,279
  • 1
  • 28
  • 45

1 Answers1

8

TypeScript doesn't provide any built-in way of doing this, so if you want something like that you have to build it yourself with mapped and conditional types. You also need to be very careful to think about exactly what behavior you want, since there are all kinds of potential edge cases, especially with optional/readonly/etc properties and functions/arrays.

Here's one way to do it:

type _Overwrite<T, U> = U extends object ? (
    { [K in keyof T]: K extends keyof U ? _Overwrite<T[K], U[K]> : T[K] } & U
) : U

type ExpandRecursively<T> = T extends Function ? T : T extends object
    ? T extends infer O ? { [K in keyof O]: ExpandRecursively<O[K]> } : never
    : T;

type Overwrite<T, U> = ExpandRecursively<_Overwrite<T, U>>

The type _Overwrite<T, U> takes a type T and U and walks down recursively through their properties, replacing ones from T with ones from U if there is a conflict. This type should work, but uses intersections to represent it, which could get ugly.

So ExpandRecursively<T> is a type that walks through the resulting type and merges all the properties together, so {a: string} & {b: number} should become {a: string, b: number}.

And Overwrite<T, U> is just taking _Overwrite<T, U> and uses ExpandRecursively<> on the result.


Let's see how it behaves with an example:

type Foo = {
    a: {
        b: {
            c: string[];
            d: number;
        }
        e: {
            f: boolean;
        }
    };
    g: {
        h?: () => string;
    }
}

type ReplaceFoo = Overwrite<Foo, { a: { b: { c: number } } }>;

This yields:

/*
type ReplaceFoo = {
    a: {
        b: {
            c: number;
            d: number;
        };
        e: {
            f: boolean;
        };
    };
    g: {
        h?: (() => string) | undefined;
    };
}
*/

Which looks reasonable to me. But you'd want to test something like this very thoroughly before using it: What do you want to do if T and/or U are union types? What do you want to do if T or U is an array or a tuple? Do you want to be able to make a distinction between "replace a.b.c with number" and "replace a.b with {c: number}"? (That is, do you want to have the option to "erase" instead of "replace" subproperties?) All of the answers to these questions (and probably other questions) will have some bearing on how you'd want to write Overwrite<T, U>.

Hope that gives you some idea how to move forward. Good luck!

Playground Link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360