Reputation: 899
I have the following component, which can either contain an image or a text. Hence in JSX I check whether there's an image (because that means there must not be text vice versa), and then either display the image or text.
How can I define this behavior with TypeScript types?
I BlockCtaDoubleData
, which is always available, and EITHER BlockCtaDoubleImageData
OR BlockCtaDoubleTextData
.
I tried the following, but Typescript keeps throwing an error for props.text
& props.image
, but not for props.headline
.
How so?
interface BlockCtaDoubleData
{
headline: string;
}
interface BlockCtaDoubleImageData extends BlockCtaDoubleData
{
image: string;
alt: string;
}
interface BlockCtaDoubleTextData extends BlockCtaDoubleData
{
text: string;
}
export type BlockCtaDoubleProps = BlockCtaDoubleTextData | BlockCtaDoubleImageData;
export function BlockCtaDouble(props: BlockCtaDoubleProps)
{
return (
<div>
{props.headline}
{props.image ? (
<img src={props.image} alt={props.alt}/>
) : (
{props.text}
)}
</div>
);
}
FYI: The error is saying that the respective prop is not existing in the counter type.
Upvotes: 0
Views: 191
Reputation: 1444
To distinguish BlockCtaDoubleImageData
and BlockCtaDoubleTextData
you will need a type guard.
import React from "react";
interface BlockCtaDoubleData {
headline: string;
}
interface BlockCtaDoubleImageData extends BlockCtaDoubleData {
image: string;
alt: string;
}
interface BlockCtaDoubleTextData extends BlockCtaDoubleData {
text: string;
}
export type BlockCtaDoubleProps =
| BlockCtaDoubleTextData
| BlockCtaDoubleImageData;
const isBlockCtaDoubleImageData = (
a: BlockCtaDoubleProps
): a is BlockCtaDoubleImageData => a.hasOwnProperty("image");
const isBlockCtaDoubleTextData = (
a: BlockCtaDoubleProps
): a is BlockCtaDoubleTextData => a.hasOwnProperty("text");
export function BlockCtaDouble(props: BlockCtaDoubleProps) {
return (
<div>
{props.headline}
{isBlockCtaDoubleImageData(props) ? (
<img src={props.image} alt={props.alt} />
) : (
props.text
)}
</div>
);
}
A type guard is a function that returns x is Type
. You need to implement it in such way: it returns true if x is Type and false otherwise. The exact condition is up to you.
You cannot achieve it with your approach. You will have to use discriminated union.
interface BlockCtaDoubleData {
headline: string;
type: "image" | "text";
}
interface BlockCtaDoubleImageData extends BlockCtaDoubleData {
image: string;
alt: string;
type: "image";
}
interface BlockCtaDoubleTextData extends BlockCtaDoubleData {
text: string;
type: "text";
}
export type BlockCtaDoubleProps =
| BlockCtaDoubleTextData
| BlockCtaDoubleImageData;
export function BlockCtaDouble(props: BlockCtaDoubleProps) {
return (
<div>
{props.headline}
{props.type === "image" ? (
<img src={props.image} alt={props.alt} />
) : (
props.text
)}
</div>
);
}
<BlockCtaDouble headline="as" text="xd" type="image" image="xd" />
will throw an error:
Type '{ headline: string; text: string; type: "image"; image: string; }' is not assignable to type '(IntrinsicAttributes & BlockCtaDoubleTextData) | (IntrinsicAttributes & BlockCtaDoubleImageData)'.
Property 'text' does not exist on type 'IntrinsicAttributes & BlockCtaDoubleImageData'.
Upvotes: 1
Reputation: 55856
Adding to the existing answer, you can use a terser behavour using in
operator. You can do the following:
interface BlockCtaDoubleData
{
headline: string;
}
interface BlockCtaDoubleImageData extends BlockCtaDoubleData
{
image: string;
alt: string;
}
interface BlockCtaDoubleTextData extends BlockCtaDoubleData
{
text: string;
}
export type BlockCtaDoubleProps = BlockCtaDoubleTextData | BlockCtaDoubleImageData;
export function BlockCtaDouble(props: BlockCtaDoubleProps)
{
return (
<div>
{props.headline}
{("image" in props) ? (
<img src={props.image} alt={props.alt}/>
) : (
{props.text}
)}
</div>
);
}
TS Playground to show the in
behaviour
Upvotes: 1