Marco
Marco

Reputation: 644

Typescript: How to filter between two similar interfaces

I am trying to write a function that can receive a couple of similar objects. Something like this:

interface BannerProps {
  type: typeof possibleBannerTypes[number];
  data?: unknown
}

Then I have a simple chain of if/else if statements that returns different UI components based on the type parameter.

function test(bannerData: EmailBounceBannerProps | BannerProps) {
    if (bannerData.type === 'anotherBanner') {
        // do something
    } else if (bannerData.type === 'emailBounce') {
        console.log(bannerData.data.reason)
    }
}

Objects with the type property set to a specific value (emailBounce) has a data property that I would also like to type so that I can use type checking and Intellisense in VS Code.

interface EmailBounceBannerProps extends BannerProps{
  type: 'emailBounce';
  data: {
    reason: string;
  };
}

However, I can't seem to make it work, typescript doesn't seem to be able to figure out when type === 'emailBounce, the data property becomes populated.

I've created a small snippet on the TypeScript playground to illustrate this problem:

Code snippet

All I'm trying to to is to get this to pass error-free, while getting the code suggestions based on EmailBounceBannerProps

error message from the above link

I'm pretty sure I've successfully done something like this before, but I can't figure out what I'm doing wrong. I'm also not sure what this practice or code pattern is called, so my googling so far hasn't led to any successful results.

Does anyone have a solution, or a better way of accomplishing this?

Upvotes: 1

Views: 424

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249656

The first problem is that possibleBannerTypes is going to be widened to string[], we need an as const to keep the the literal types.

The second problem is that since you union with BannerProps on the branch bannerData.type === 'emailBounce' you haven't really narrowed anything, since bannerData could still be either EmailBounceBannerProps or BannerProps. And this means data will be unknown | { reason: string } which will just be unknown.

You need a default case that excludes any of the known cases from the type property. You could use Exclude to type the type of this default interface and use it instead of BannerProps in the union:

const possibleBannerTypes = ['emailBounce', 'anotherBanner'] as const;

interface BannerProps {
  type: typeof possibleBannerTypes[number];
  data?: unknown
}

interface DefaultBannerProps extends BannerProps {
  type: Exclude<typeof possibleBannerTypes[number], EmailBounceBannerProps['type']>; // really just 'anotherBanner' 
}

interface EmailBounceBannerProps extends BannerProps{
  type: 'emailBounce';
  data: {
    reason: string;
  };
}

function test(bannerData: EmailBounceBannerProps | DefaultBannerProps) {
    if (bannerData.type === 'anotherBanner') {
        // do something
    } else if (bannerData.type === 'emailBounce') {
        console.log(bannerData.data.reason)
    }
}

Upvotes: 2

Related Questions