Typescript union types & inferring differing properties without extra type checks

Viewed 97

I am struggling a bit with union types and am wondering if typescript can infer the value without the extra checks all over the place. Lets say I have this interface setup (left out IBaseDiscount for brevity) where the values can be different but the nested discount type names are fixed

interface IFlatDiscount extends IBaseDiscount {
    value:{ formatted: string; value: number };
    discountType: {
        name: DiscountType.flat;
        id: number;
    };
}

interface IOpenDiscount extends IBaseDiscount {
    value?: number;
    discountType: {
        name: DiscountType.open;
        id: number;
    };
}

export interface IPercentageDiscount extends IBaseDiscount {
    value: number;
    discountType: {
        name: DiscountType.percentage;
        id: number;
    };
}

export type IDiscount = IOpenDiscount | IPercentageDiscount | IFlatDiscount;

Now in my code when I am trying use these values I end up having to do stuff like the following

if (discount.discountType.name === DiscountType.flat && typeof discount.value === 'object) {
    // now my value is properly typed -- if I leave out the object check it doesnt know the correct type for the value
}

Is there a proper way for typescript to infer the value based off the discountType.name instead of having all the checks on the value everywhere?

1 Answers

First, you're doing unnecessary type checking here:

if (discount.discountType.name === DiscountType.flat && 
  typeof discount.value === 'object') {
}

The following would be enough:

if (typeof discount.value === 'object') {
  
}

If you want to decide interface according to discountType.name, typescript cannot infer type from nested property type checking.

There are basically two ways:

  • You use "as" keyword (because you're sure that the type is correct):
switch(discount.discountType.name) {
  case DiscountType.flat:
    console.log((discount as IFlatDiscount).value.formatted);
    break;
  case DiscountType.open:
    console.log((discount as IOpenDiscount).value);
    break;
  case DiscountType.percentage:
    console.log((discount as IPercentageDiscount).value);
}
  • Even better, you can use generic type:
export enum DiscountType {
    flat,
    open,
    percentage
}


export interface IGenericDiscount<T extends DiscountType> {
    value: T extends DiscountType.flat 
        ? { formatted: string; value: number } 
        : 
            (T extends DiscountType.open 
                ? (number | undefined) 
                : number
            );
    discountType: {
        name: T;
        id: number;
    }
}

const genericDiscount: IGenericDiscount<DiscountType.open> = JSON.parse('{}');

console.log(genericDiscount.value);

Typescript playground

Related