0

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.

2 Answers 2

1

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.

Edit with answer for question from comments

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'.
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! This makes it work. One more thing though - calling the component, I'm still able to mix the probs, how can I make TypeScript tell me, that < BlockCtaDouble headline="Test" image="some image" text="some text"> is not possible.
I suppose you would need a generic component for that, will check if it is possible in TSX.
@Tim I've added possible solution.
1

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

3 Comments

Thanks, how can I additionally tell TS that calling the component like <BlockCtaDouble headline="Test" image="some image" text="some text"/> (basically just mixing types again) is forbidden?
Being a structural typing, TypeScript does not stop you from having extra fields. So, a type T = {a : string} is compatible with {a: 'adc', extra: 123}. In your case, BlockCtaDoubleProps is valid as long as it contains headline, image, alt attributes, if you add extra attribute names text, it is still valid BlockCtaDoubleProps. Similarly, if it contains headline, text keys, it is valid BlockCtaDoubleProps, but if happen to contain image and / or alt as extra key, it is valid.
@Nishant is right. Although you may use discriminated union to achieve it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.