Can you do smart types in Typescript?

Viewed 179

Let's say I have in interface like this:

export interface Promotion {
  title: string;
  image?: string;
  description?: string;
  startDate?: string;
  endDate?: string;
  type: 'discountWithImage' | 'discount' | 'countdown';
}

Now my questions is, since I now for sure that if the key type is equal to 'countdown' then the endDate and the startDate will not be undefined. Is there a way to do it in typescript? so something like:

export interface Promotion {
  title: string;
  image?: string;
  description?: string;
  startDate?: string;
  endDate: Promotion.type === 'countdown' ? string : undefined;
  type: 'discountWithImage' | 'discount' | 'countdown';
}

example to explain on playground Playground

1 Answers

You can do this with a generic type parameter. Think of it like and argument that you pass into your type.

type PromotionType = 'discountWithImage' | 'discount' | 'countdown';

export interface Promotion<T extends PromotionType> {
  title: string;
  image?: string;
  description?: string;
  startDate?: string;
  endDate: T extends 'countdown' ? string : undefined;
  type: T;
}

You would use it like so:

declare const testA: Promotion<'discount'>
const endDateA = testA.endDate // undefined

declare const testB: Promotion<'countdown'>
const endDateB = testB.endDate // string

You can even use a generic function to pass that type parameter for you:

function getPromotion<T extends PromotionType>(type: T): Promotion<T> {
  return {} as Promotion<T> // Not a real implementation
}

const resultA = getPromotion('discount').endDate // undefined
const resultB = getPromotion('countdown').endDate // string

Playground


Or you could define a discriminated union with the a slightly different interface depending on the value of type.

// One interface for common values that all promotions share
export interface PromotionCommon {
  title: string;
  image?: string;
  description?: string;
  startDate?: string;
}

// The discount promotion type has no end date and discount type.
export interface PromotionDiscount extends PromotionCommon {
  endDate: undefined;
  type: 'discountWithImage' | 'discount';
}

// The countdown promotion type has an end date and a countdown type.
export interface PromotionCountdown extends PromotionCommon {
  endDate: string;
  type: 'countdown';
}

// The final Promotion type can be either a discount or a countdown
type Promotion = PromotionDiscount | PromotionCountdown


declare const promotion: Promotion // pretend we have a promotion of unknown promotion type
if (promotion.type === 'countdown') {
  const endDate = promotion.endDate // type: string
} else {
  const endDate = promotion.endDate // type: undefined
}

Playground

Related