2

I have two class: RewardArticleBase & RewardArticle

export class RewardArticleBase extends Reward {
}

export class RewardArticle extends Reward {
    public title: string = '';
    public images: string[] = [];
}

and a config function that can deal with both of these two types. the difference between them is RewardArticleBase does not have title field. It seems not correct to add title?, but now it says RewardArticleBase and RewardArticle do not have title property and index signature.

function getRewardConfig({ content, reward_amount, title }: RewardArticle | RewardArticleBase) {
    if (title) {
        // todo
    }
    // other stuff
}
Gikono
  • 135
  • 9

1 Answers1

2

Unions only allow access to common fields. This makes sense because without any extra checks there is no way to know that any of the other fields exist.

While this behavior is by design and is generally a good thing, we can look a union in a slightly different way. We can look at is as a type that has all common fields, but also has the non-common fields only marked as optional:

type A =  { common : string, fieldA: string }
type B =  { common : string, fieldB: string }
// We would like to create a type equivalent to 
type AB = { common : string, fieldA?: string, fieldB?: string }

Given such a type we could perform the argument deconstruction in a type safe way (with the non-common fields requiring an extra null check if we use strictNullChecks)

To create the desired type without explicitly redefining it we first need a way to transform a union to an intersection (in order to have access to all fields). Fortunately the type UnionToIntersection<U> in this answer provides us with a way to do this (don't forget to upvote @jcalz's answer its a truly great unsung answer :) ).

Armed with UnionToIntersection we can create a type which contains the common keys (which is actually just the original union) in an intersection with the non-common fields using Pick to take only the uncommon fields from the intersection and Exclude to get the keys of the uncommon fields, and finally applying Partial to mark all the non common fields as optional:

type UnionToIntersection<U> = 
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type DeconstructUnionHelper<T> = T & Partial<Pick<UnionToIntersection<T>, Exclude<keyof UnionToIntersection<T>, keyof T>>> 

Usage :

export class Reward {
    content: string = "";
    reward_amount = "";
}
export class RewardArticleBase extends Reward {
}

export class RewardArticle extends Reward {
    public title: string = '';
    public images: string[] = [];
}
function getRewardConfig({ content, reward_amount, title }: DeconstructUnionHelper<RewardArticle | RewardArticleBase>) {
    if (title) { // title is string | undefined since it's optional
        // todo
    }
    content // is string
    // other stuff
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357