Reputation: 644
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:
All I'm trying to to is to get this to pass error-free, while getting the code suggestions based on EmailBounceBannerProps
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
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