Home > front end >  Destructuring props from Utility Types (Extract and Exclude - TypeScript and React)
Destructuring props from Utility Types (Extract and Exclude - TypeScript and React)

Time:08-06

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 on string

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}
    />
  )
}
  • Related