I'm facing a problem with Typescript and React.
Let's supposed I have the following Union Type:
type Fruits = 'apple' | 'peach' | 'banana';
And I use it like this:
type FoodBaseProps = {
fruits: Fruits;
weight: number
price: number
}
Now, imagine that I have a boolean prop called needToPeel
that should only be informed if the fruit banana
is passed;
To solve that I did the following:
export type NeedToPeelFruitsProps = FoodBaseProps & {
fruits: Extract<Fruits, 'banana'>;
needToPeel?: boolean;
};
And for the other fruits that do not need to be peeled I have:
export type DefaultFruitsProps = FoodBaseProps & {
fruits: Exclude<Fruits, 'banana'>;
};
And then I have:
type FoodProps = NeedToPeelFruitsProps | DefaultFruitsProps;
Now, if I do some assertions based on this logic I'll have this (working as expected):
const testFn = (food: FoodProps) => {
switch (food.fruits) {
case 'banana': {
console.log(food.needToPeel); // No error
break;
}
case 'apple': {
console.log(food.needToPeel); // TS2339: Property 'needToPeel' does not exist on type 'DefaultFruitsProps'.
break;
}
default:
}
};
However, if I try to access this needToPeel
prop in my React component, I'll have some problems:
const Food = ({
fruits,
weight,
price,
needToPeel, // TS2339: Property 'needToPeel' does not exist on type 'FoodProps'.
}: FoodProps) => {
return (
<FoodBase
fruits={fruits}
weight={weight}
price={price}
needToPeel={needToPeel} // 2 errors below
/>
)
}
1 - TS2769: No overload matches this call. Overload 1 of 2, '(props: Omit<Omit<Pick<DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "key" | keyof HTMLAttributes<...>> & { ...; } & [...]
;
2 - TS2339: Property 'needToPeel' does not exist on type 'FoodProps'.
I think the problem is because union types only consider the props in common. So, needToPeel
prop will not be accessible since it's not shared among the types.
I was wondering if there's a way to be able to access the needToPeel
in my React component.
CodePudding user response:
I think the problem is because union types only consider the props in common.
Indeed, when a variable is typed as a union, the only safe thing we know is that it has the common properties.
TypeScript will only allow an operation if it is valid for every member of the union. For example, if you have the union
string | number
, you can’t use methods that are only available onstring
The only safe way to access the specific properties, is to first narrow the type, e.g. like in your "assertion" example.
The solution is to narrow the union with code, the same as you would in JavaScript without type annotations. Narrowing occurs when TypeScript can deduce a more specific type for a value based on the structure of the code.
if I try to access this
needToPeel
prop in my React component, I'll have some problems
That is because if we do the typical props destructuring of React functional components from the function argument definition (like in the question example), this destructuring occurs too early. If we want to destructure and get the specific needToPeel
property, it must occur after type narrowing.
I would consider such situation a legit reason to deviate from this standard practice: as implied, there is no hard technical requirement to destructure the props from the function argument definition; it is rather a common practice for React functional components; in React class-based components, we have to use this.props
anyway.
One of the very first examples of component with props in React documentation also does not use destructuring:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
So in your case, you can very well postpone the destructuring to after type narrowing (or even not destructure at all):
const Food = (props: FoodProps) => {
// Narrowing
if (props.fruits === "banana") {
// Now TS knows that props is a NeedToPeelFruitsProps
return (
<FoodBase
fruits={props.fruits}
weight={props.weight}
price={props.price}
needToPeel={props.needToPeel} // Okay
/>
);
}
// Depending on the signature of <FoodBase>,
// you could even blindly pass the props,
// which may or may not have the needToPeel property
return (
<FoodBase
{...props}
/>
)
}