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?
CodePudding user response:
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
.
CodePudding user response:
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>;
};
CodePudding user response:
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>
}