Typescript how to create type with common properties of two types?

Based on @kube's answer, you could use generics to create a reusable type:

type Common<A, B> = {
    [P in keyof A & keyof B]: A[P] | B[P];
}

This allows you to create intersections on the fly:

const c: Common<T1, T2> = { y: 123 };

Common Properties

Use static keyof operator:

type Ka = keyof A // 'x' | 'y'
type Kb = keyof B // 'y' | 'z'
type Kc = Ka & Kb // 'y'

And define a Mapped Type with properties in Kc:

type C = {
  [K in keyof A & keyof B]: A[K] | B[K]
}

This defines a new type where each key will be present both in A and B.

Each value associated to this key will have type A[K] | B[K], in case A[K] and B[K] are different.


Common Properties with Same Types only

Use Conditional Type to map key to value only if type same in A and B:

type MappedC = {
  [K in keyof A & keyof B]:
    A[K] extends B[K] // Basic check for simplicity here.
    ? K // Value becomes same as key
    : never // Or `never` if check did not pass
}

From this object, get union of all values, by accessing all keys:

// `never` will not appear in the union
type Kc = MappedC[keyof A & keyof B]

Finally:

type C = {
  [K in Kc]: A[K]
}

Shortcomings of a naive approach

While the below type is commonly suggested in the other answers:

type Common<A, B> = {
    [P in keyof A & keyof B]: A[P] | B[P];
}

It fails to check whether the property values are assignable to one another. This implies that it is possible for Common<A, B> to have properties that are not really shared by A and B.

type A = { a: number; x: string; z: number }
type B = { b: number; x: number; z: number}
Common<A, B> // => { x: string | number; z: number}
// above should just be { z: number }, since the type of property x is not
// assignable to the type of property x in both A and B.

The reason this sucks is because destructuring an object of type Common<A, B> into an object of either type A or type B can fail for either A or B. For example,

const sharedProps: Common<A, B> = { x: 'asdf', z: 9 }
const a: A = { ...sharedProps, a: 1 }

// below throws type error; string not assignable to number
const b: B = { ...sharedProps, b: 1 }

This is confusing, and puts an arbitrary limit on what we can do with the generic type.

In-depth approach that covers the above use-case

I suggest the below type (available for TS4.1+):

/**
 * Omits properties that have type `never`. Utilizes key-remapping introduced in
 * TS4.1.
 *
 * @example
 * ```ts
 * type A = { x: never; y: string; }
 * OmitNever<A> // => { y: string; }
 * ```
 */
type OmitNever<T extends Record<string, unknown>> = {
  [K in keyof T as T[K] extends never ? never : K]: T[K];
};

/**
 * Constructs a Record type that only includes shared properties between `A` and
 * `B`. If the value of a key is different in `A` and `B`, `SharedProperties<A,
 * B>` attempts to choose a type that is assignable to the types of both values.
 *
 * Note that this is NOT equivalent to `A & B`.
 *
 * @example
 * ```ts
 * type A = { x: string; y: string; }
 * type B = { y: string; z: string }
 * type C = { y: string | number; }
 *
 * A & B                  // => { x: string; y: string; z: string; }
 * SharedProperties<A, B> // => { y: string; }
 * SharedProperties<B, C> // => { y: string | number; }
 * ```
 */
type SharedProperties<A, B> = OmitNever<Pick<A & B, keyof A & keyof B>>;

This type correctly returns shared properties that are guaranteed to be subtypes of both A and B, since A & B is guaranteed to be a subtype of A and B so long as A & B is not never.

type A = { a: number; x: string; z: number }
type B = { b: number; x: number; z: number}
SharedProperties<A, B> // => { z: number }

Tags:

Typescript