Johnny Kontrolletti
Johnny Kontrolletti

Reputation: 899

Typescript implement conditional props

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

Answers (2)

Łukasz Szcześniak
Łukasz Szcześniak

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.

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'.

Upvotes: 1

Nishant
Nishant

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

Related Questions