Joji
Joji

Reputation: 5625

TypeScript: is there a way to extends multiple interfaces and merge identical properties?

I have two interfaces A and B and they have one property in common

interface A {
  a: boolean;
  b: number;
  c: string;
  d: boolean;
}

interface B {
  d: string;
}

interface C extends A,B {}

Ideally I want C to be

{
  a: boolean;
  b: number;
  c: string;
  d: boolean | string; <- merged
}

Is there a way to do that? I know I can use Pick to left out the identical key and then extends it but is there any other way to do it?

Upvotes: 9

Views: 5509

Answers (3)

Mingwei Samuel
Mingwei Samuel

Reputation: 3272

Here you go:

type Merge<X, Y> = {
    [K in (keyof X | keyof Y)]:
        (K extends keyof X ? X[K] : never)
        | (K extends keyof Y ? Y[K] : never)
};

Example usage:

interface A {
  a: boolean;
  b: number;
  c: string;
  d: boolean;
}

interface B {
  d: string;
}

type C = Merge<A, B>;
// Result:
type C = {
    a: boolean;
    b: number;
    c: string;
    d: string | boolean;
}

Playground Link

Upvotes: 5

Paul Huynh
Paul Huynh

Reputation: 3150

It is possible, and I'll try to explain each part of my answer.

/** Returns keys in the left type which are not in the right type. */
type UniqueLeftKeys<T, U> = Exclude<keyof T, keyof U>
/** Returns keys in the right type which are not in the left type. */
type UniqueRightKeys<T, U> = Exclude<keyof U, keyof T>
/** Returns keys that exist in BOTH the left and right types. */
type CommonKeys<T, U> = keyof T & keyof U
type Merge<T, U> = {
  [k in UniqueLeftKeys<T, U>]: T[k] // First part
} & {
    [k in UniqueRightKeys<T, U>]: U[k] // Second part
  } & {
    [k in CommonKeys<T, U>]: T[k] | U[k] // Third part
  }

The Secret is in the Merge type, which consists of 3 parts.

The first part takes the keys that are only in the LEFT type and not in the RIGHT type, and add those properties to our result. In your example, the properties a, b, c are only in interface A, so we add those three to our resulting type.

The second part takes the keys that are only in the RIGHT type and not in the LEFT type, and add those properties to our result. In your example, the right type B does not contain any keys that aren't already in A, so we don't add anything to the resultant type.

The third part gets the keys that are in BOTH types, and then add those to the resultant type, where the type of those keys is a union of both types.

You can now use this code like so:

type C = Merge<A, B>
const c: C = {
  a: false,
  b: 1,
  c: 'abc',
  d: false // NOTE: we can also use string here
}

You could easily simplify this code by inlining those extra type definitions (UniqueLeftKeys, UniqueRightKeys, and CommonKeys), but I've left those in the example as it helps to explain what is happening at each step

Upvotes: 2

xavdid
xavdid

Reputation: 5262

Since A and B conflict, I believe you can't combine them. If you know what the conflicting keys are, you can omit them when declaring C:

interface A {
  a: boolean;
  b: number;
  c: string;
  d: boolean;
}

interface B {
  d: string;
}

interface C extends Omit<A, 'd'>, Omit<B, 'd'> {
  d: string | boolean
}

let c: C = {
  a: true, b: 3, c: 'a', d: 'a'
}

Upvotes: 4

Related Questions