ThomasReggi
ThomasReggi

Reputation: 59365

Generic that creates interface with overwritten properties

Given a Person type

type Person = {
  name: string,
  age: number,
  address: {
    line1: string,
    line2: string | null | number,
    zip: string | number,
  },
};

I'd like to apply overwrites and modify this interface.

type PersonOverwrites = {
  address: {
    line2?: string,
    zip: string,
  },
};

From these two interfaces I would like to create this:

type PersonModified = {
  name: string,
  age: number,
  address: {
    line1: string,
    line2?: string,
    zip: string,
  },
}

This is what I am looking for:

type PersonModified = Overwrite<Person, PersonOverwrites>

How can I create this type of Generic?

Update

This should complain:

type DeepMerge<T, U> = [T, U] extends [object, object] ?
  {
    [K in keyof (U & Pick<T, Exclude<keyof T, keyof U>>)]: (
      K extends keyof U ? (
        K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
      ) : (
        K extends keyof T ? T[K] : never
      )
    )
  } : U;


type Person = {
  name: string,
  age: number,
  address: {
    line1: string,
    line2: string | null | number,
    zip: string | number,
    address?: {
        line1: string,
        line2: string | null | number,
        zip: string | number,
        address?: {
            line1: string,
            line2: string | null | number,
            zip: string | number,
        },
    },
  },
};

type PersonOverwrites = {
    address: {
        line2?: string,
        zip: string,
        address?: {
            address: {
                pizzaDelivery: boolean,
            },
        },
    },
};

const person: Person = {
    name: 'Thomas',
    age: 12,
    address: {
        line1: 'hi',
        line2: 'hi',
        zip: 'hi',
        address: {
            line1: 'hi',
            line2: 'hi',
            zip: 'hi',
            address: {
                line1: 'hi',
                line2: 'hi',
                zip: 'hi',
                // pizzaDelivery: true,
            }
        }
    }
}

Upvotes: 1

Views: 37

Answers (2)

ThomasReggi
ThomasReggi

Reputation: 59365

Building of @jcalz answer I believe this fixes the optional issue.

type Merge<M, N> = Omit<M, Extract<keyof M, keyof N>> & N;

type DeepMerge<T, U> = [T, U] extends [object, object] ?
  {
    [K in keyof (Merge<T, U>)]: (
      K extends keyof U ? (
        K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
      ) : (
        K extends keyof T ? T[K] : never
      )
    )
  } : Merge<T, U>;

Upvotes: 0

jcalz
jcalz

Reputation: 328262

I don't know how deeply nested you want this to be, but you might want something like this:

type DeepMerge<T, U> = [T, U] extends [object, object] ?
  {
    [K in keyof (U & Pick<T, Exclude<keyof T, keyof U>>)]: (
      K extends keyof U ? (
        K extends keyof T ? DeepMerge<T[K], U[K]> : U[K]
      ) : (
        K extends keyof T ? T[K] : never
      )
    )
  } : U;

type PersonModified = DeepMerge<Person, PersonOverwrites>

The idea is that DeepMerge<T, U> either evaluates to just U if either T or U is not an object type, or it walks through each combined key of T or U and merges more deeply. The specifics around how it determines which properties should be optional, and how it decides when to stop merging are straightforward but tedious to explain. If you need something specific elaborated, let me know.

You can verify that PersonModified is equivalent to

type PersonModified = {
    address: {
        line2?: string | undefined;
        zip: string;
        line1: string;
    };
    name: string;
    age: number;
}

as you wanted.

Hope that helps; good luck!

Upvotes: 3

Related Questions