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 }