0

I am trying to create a reusable component that takes one of two different types, let's call them Car and Truck that we get from API.

type Props = {
  data: Car | Truck;
  onToggleDataSave: (id: number, isHome: boolean) => void;
  isCar: boolean;
};

const Card = ({ data, onToggleDataSave, isCar }: Props) => {
  const handleToggleCardSave = (e: SyntheticEvent) => {
    e.preventDefault();
    isCar
      ? onToggleDataSave(data.carId, isCar)
      : onToggleDataSave(data.truckId, isCar);
  };


  return (
    <h1> Some markup </h1>
  );
};

This works in JavaScript as I am making a check if my data is a car before I try to access its property, but TS isn't happy about it as it says that Truck doesn't have carId property.

How can I satisfy TypeScript while keeping my component reusable?

3 Answers 3

1

This doesn't require any additional checks, just better types.

type Car = { carId: number }
type Truck = { truckId: number }

type PropsInCommon = {
  onToggleDataSave: (id: number, isHome: boolean) => void;
}

type Props = ({
  data: Car;
  isCar: true;
} | { 
  data: Truck;
  isCar: false
}) & PropsInCommon;

const Card = ({ data, onToggleDataSave, isCar }: Props) => {
  const handleToggleCardSave = (e: SyntheticEvent) => {
    e.preventDefault();
    isCar
      ? onToggleDataSave(data.carId, isCar)
      : onToggleDataSave(data.truckId, isCar);
  };


  return (
    <h1> Some markup </h1>
  );
};

Even though this may feel a little roundabout, it feels like it expresses your intent without requiring a type guard.

This code essentially tells the compiler that the isCar: true, data: Truck state is not valid using the union operator.

This better informs the compiler the expected values of this type:

When isCar is true, data has type Car.

Sign up to request clarification or add additional context in comments.

Comments

1

The other answers looks good, but do you really need the isCar props that you are passing to your component. You can decide if it is a car or not by checking the type like this:

    type Car = {
      carId: number;
    };
    
    type Truck = {
      truckId: number;
    };
    
    interface Props {
      vehicle: Car | Truck;
    }


    const isCar = (v: any): v is Car => {
      return v.carId !== undefined;
    };
    
    const Vehicle = (props: Props) => {
    
      return (
        <div>
          {isCar(props.vehicle) ? props.vehicle.carId : props.vehicle.truckId}
        </div>
      );
    };
    
    export default function App() {
      const vehicle = { carId: 1 } as Car;
    
      return (
        <div className="App">
          <h1>Vehicle</h1>
          <Vehicle vehicle={vehicle} />
        </div>
    }

Comments

0
type Props = {
    data: Car | Truck;
    onToggleDataSave: (id: number, isHome: boolean) => void;
    isCar: boolean;
};

function carGuard(_data: Car | Truck, isCar: boolean): _data is Car {
    return isCar;
}

export const Card = ({ data, onToggleDataSave, isCar }: Props): JSX.Element => {
    const handleToggleCardSave = (e: SyntheticEvent): void => {
        e.preventDefault();
        if (carGuard(data, isCar)) {
            onToggleDataSave(data.carId, isCar);
        } else {
            onToggleDataSave(data.truckId, isCar);
        }
    };

    return <h1> Some markup </h1>;
};

2 Comments

Would this be a proper convention to follow when building React components with TS?

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.