skip to Main Content

I’m encountering a TypeScript error when using conditional types with React components.

The issue arises when trying to render different types of components based on a type prop and providing the corresponding props for each type.

type PairingCardProps = {
  p: string;
};

type SampleCardProps = {
  s: string;
};

export const GRID_CARD_COMPONENTS = {
  pairing: ({ p }: PairingCardProps) => <div>Pairing Card</div>,
  sample: ({ s }: SampleCardProps) => <div>Sample Card</div>,
};

type TypefaceGridCardProps =
  | ({ type: "sample" } & SampleCardProps)
  | ({ type: "pairing" } & PairingCardProps);

const TypefaceGridCard = ({ type, ...props }: TypefaceGridCardProps) => {
  const Card = GRID_CARD_COMPONENTS[type as keyof typeof GRID_CARD_COMPONENTS];

  if (!Card) return null;

  return <Card {...props} />;
};

The error I encounter is as follows:

Type PairingCardProps is missing the following properties from type 'SampleCardProps': s

Workaround

I work around this issue by type casting but find the object map solution above preferable.

type TypefaceGridCardProps =
  | ({
      type: "pairing";
    } & PairingCardProps)
  | ({
      type: "sample";
    } & SampleCardProps);

const TypefaceGridCard = ({ type, ...props }: TypefaceGridCardProps) => {
  switch (type) {
    case "sample":
      return <SampleCard {...(props as SampleCardProps)} />;

    case "pairing":
      return <PairingCard {...(props as PairingCardProps)} />;

    default:
      throw new Error(`Unknown type: ${type}`);
  }
};

2

Answers


  1. I think this us due to typescript as typescript compiler is not able to infer the Card component in the TypefaceGridCard component. I also face this type of issue . So this can be solved as :

    import React from "react";
    
    type PairingCardProps = {
      p: string;
    };
    
    type SampleCardProps = {
      s: string;
    };
    
    export const GRID_CARD_COMPONENTS = {
      pairing: ({ p }: PairingCardProps) => <div>Pairing Card</div>,
      sample: ({ s }: SampleCardProps) => <div>Sample Card</div>,
    };
    
    type TypefaceGridCardProps =
      | ({ type: "sample" } & SampleCardProps)
      | ({ type: "pairing" } & PairingCardProps);
    
    const TypefaceGridCard = ({ type, ...props }: TypefaceGridCardProps) => {
      const Card = GRID_CARD_COMPONENTS[type as keyof typeof GRID_CARD_COMPONENTS];
    
      if (!Card) return null;
    
      return <Card {...props as any} />;
    };
    
    export default TypefaceGridCard;
    
    Login or Signup to reply.
  2. The compiler is unable to handle the "correlated union types" described in ms/TS#30581, however, there is a suggested refactor described in ms/TS#47109, which considers moving to generics.
    First, we will need some map type that will hold props of every component:

    type PairingCardProps = {
      p: string;
    };
    
    type SampleCardProps = {
      s: string;
    };
    
    type TypeMap = {
      sample: SampleCardProps;
      pairing: PairingCardProps;
    };
    

    Now, let’s redefined GRID_CARD_COMPONENTS using mapped type with using TypeMap as a source for types:

    export const GRID_CARD_COMPONENTS: { [K in keyof TypeMap]: React.FC<TypeMap[K]> } = {
      pairing: ({ p }) => <div>Pairing Card</div>,
      sample: ({ s }) => <div>Sample Card</div>,
    };
    

    Let’s also define the type of the GRID_CARD_COMPONENTS which we will need afterwards:

    type GridCardComponents = typeof GRID_CARD_COMPONENTS;
    

    We should also create a mapped type for arguments of TypefaceGridCard, which will accept an option generic argument K that extends the key of TypeMap and is defaulted to keyof TypeMap. This type will return the props for the given K or a union of all possible props:

    import {ComponentProps} from 'react';
    // ...
    type TypefaceGridCardProps<T extends keyof TypeMap = keyof TypeMap> = {
      [K in T]: { type: K } & ComponentProps<GridCardComponents[K]>;
    }[T];
    

    Let’s apply our types to the TypefaceGridCard:

    const TypefaceGridCard = <T extends keyof TypeMap>(props: TypefaceGridCardProps<T>) => {
      const Card = GRID_CARD_COMPONENTS[props.type];
    
      if (!Card) return null;
    
      Card(props); // no error
      
      return <Card {...props} />; // unexpected error
    }
    

    Strangely, we get an error when we try to invoke the component in JSX format. I’m not sure about the exact reason, but I think it has to do something about React’s internal generic types, which can’t properly handle such cases.

    The workaround is to manually type Card as FC<TypeMap[K]> and define another variable for props that is typed as ComponentProps<typeof Card> and has a value of props:

    const TypefaceGridCard = <T extends keyof TypeMap>(props: TypefaceGridCardProps<T>) => {
      const Card: FC<TypeMap[T]> = GRID_CARD_COMPONENTS[props.type];
      const correctlyTypedProps: ComponentProps<typeof Card> = props;
    
      if (!Card) return null;
    
      return <Card {...correctlyTypedProps} />; // no error
    };
    

    Alternatively, you could use as assertion instead of creating another variable, but I wouldn’t recommend it, since it is less type-safe:

    const TypefaceGridCard = <T extends keyof TypeMap>(props: TypefaceGridCardProps<T>) => {
      const Card: FC<TypeMap[T]> = GRID_CARD_COMPONENTS[props.type];
    
      if (!Card) return null;
    
      return <Card {...(props as ComponentProps<typeof Card>)} />; // no error
    };
    

    Link to StackBlitz

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search