I have following three interfaces:
type A = {
x: string
y: string
}
type B = {
y: string
}
type C = {
z: string;
y: string;
}
There are common required properties here. And there're required properties which are not part of the rest of 2 types. So, my question is:
Is there way to create type that unites common props and makes other properties optional? E.g.:
type D = {
y: string
x?: string
z?: string
}
Any ideas?
CodePudding user response:
There's quite a few ways to do this, but one solution would be to create a type Pick<T1, keyof T1 & keyof T2 & keyof T3>
that has the properties shared by three types T1
to T3
, and intersect it with an object Partial<T1 & T2 & T3>
that has optional properties for any properties in T1
to T3
.
type CommonRequiredRestOptional<T1, T2, T3> =
Pick<T1, keyof T1 & keyof T2 & keyof T3> & Partial<T1 & T2 & T3>
The partial object will also contain the required properties, but because of the intersection these stay required (T & (T | undefined)
is T
).
A helper TopLevelNormalize
can be used to show the actual object type rather than Pick<A, "y"> & Partial<A & B & C>
:
type TopLevelNormalize<T> = T extends infer S ? {[K in keyof S]: S[K]} : never;
type Test = TopLevelNormalize<CommonRequiredRestOptional<A, B, C>>
// type Test = { y: string; x?: string | undefined; z?: string | undefined;}
CodePudding user response:
Whilst working on this solution, I tried to create an implementation that used generics but didn't get anywhere. I'm sure that a TypeScript wizard could derive a generic solution from mine!
First, let's create a utility type for finding properties that are "common":
type Common = {
[key in keyof A & keyof B & keyof C]: A[key] | B[key] | C[key]
}
Now we can find properties from one of your interfaces that are "uncommon":
{
// Properties only in A
[key in keyof Exclude<A, keyof Common>]?: A[key]
}
Putting this altogether, we get this:
export interface A {
x: string
y: string
}
export interface B {
y: string
}
export interface C {
z: string
y: string
}
/**
* Common properties in A, B, and C.
*/
type Common = {
[key in keyof A & keyof B & keyof C]: A[key] | B[key] | C[key]
}
/**
* All properties from A, B, and C.
*
* Properties that are "common" (in all interfaces) are required,
* others (not present in all interfaces) are optional.
*/
type D = Common & {
// Properties only in A
[key in keyof Exclude<A, keyof Common>]?: A[key]
} & {
// Properties only in B
[key in keyof Exclude<B, keyof Common>]?: B[key]
} & {
// Properties only in C
[key in keyof Exclude<C, keyof Common>]?: C[key]
}
CodePudding user response:
I wanted to share a more general answer just in case someone finds it useful.
Quite extensive, but in the end you will only need to use the CommonUncommon
type (for lack of a better name)
export interface A {
x: string;
y: string;
}
export interface B {
y: string;
}
export interface C {
z: string;
y: string;
}
export interface D {
y: string;
justAnotherProp: unknown[];
}
type IsInAll<T extends unknown[], Prop extends number | string | Symbol> = T extends [infer Head, ...infer Tail]
? Prop extends keyof Head
? IsInAll<Tail, Prop>
: false
: true;
type ExcludeArr<T extends unknown[], X, Res extends unknown[] = []> = T extends [infer Head, ...infer Tail]
? Head extends X
? X extends Head
? ExcludeArr<Tail, X, Res>
: ExcludeArr<Tail, X, [...Res, Head]>
: ExcludeArr<Tail, X, [...Res, Head]>
: Res;
type Normalize<T> = {[K in keyof T]: T[K]};
type Unite<T extends unknown[], K extends unknown[] = T, Res = unknown> = T extends [infer Head, ...infer Tail]
? Unite<Tail, K, {
[Prop in keyof Head as true extends IsInAll<ExcludeArr<K, Head>, Prop> ? Prop : never]: Head[Prop];
}
&
{
[Prop in keyof Head as false extends IsInAll<ExcludeArr<K, Head>, Prop> ? Prop : never]?: Head[Prop];
}
&
Res>
: Res;
type CommonUncommon<T extends unknown[]> = Normalize<Unite<T>>;
type res1 = CommonUncommon<[A, B]>;
type res2 = CommonUncommon<[A, B, C]>;
type res3 = CommonUncommon<[A, B, C, D]>;