Stop destructuring component props in TypeScript

ยท

5 min read

Short rant

I sort of hate object destructuring in TypeScript. In JavaScript, it's fine. But not really in TypeScript. It doesn't seem suited for proper type-safe TypeScript code.

Let me try and explain why.

When I asked ChatGPT "Is destructuring props in React a good practice?", it said:

Benefits of Destructuring Props:

  1. Readability and Clarity: Destructuring props can make your code more readable and self-explanatory. It provides a clear visual representation of which props a component is using.

  2. Less Boilerplate: Destructuring allows you to extract specific props you need, reducing the need to repeatedly reference props throughout your component.

  3. Code Consistency: Consistently using destructuring throughout your codebase can improve code consistency and make it easier for other developers to understand your components.

  4. Avoid Mistakes: Destructuring can help catch typos or errors in prop names early since destructured props that are not passed will be undefined.

I find it hard to agree with these points.

  1. Erm... no. In large components I lose clarity on the data flow and where my objects are coming from. There is nothing self-explanatory to me about not using props.thing instead of thing. Try debugging large components and see how easy it is to understand data flow and spot what state changes would trigger a re-render when reading thingA and thingB everywhere in the code and keeping that all in your head, instead of props.thingA and props.thingB, which you spot easily, or use Find props. and have all occurrences highlighted.

  2. Really? Typing props is boilerplate now? You might as well not name your variables then. Most IDEs will suggest props as soon as you type two letter, like pr, and then you're one TAB key away from props. Better yet, in IDEs like VSCode, the . key acts as a commit key, so typing . instead of TAB will immediately complete to props. with the dot added and autocomplete suggestions already opened!

  3. So does using props everywhere. And in my opinion, using props makes things easier to understand compared to destructuring.

  4. Oh, JavaScript ๐Ÿ˜‚... This is why we use TypeScript, so we don't have to check every object if it's undefined, or worse, have to debug this error Uncaught TypeError: Cannot read properties of undefined (reading 'test') for the 2.274.676th time.

In JavaScript, writing ({ propA, propB }) is like fake typing the props minus the types (at least you know what props should exist), while the equivalent in TypeScript is just a simple type: type ComponentProps = { propA: type, propB: type }.

Honestly, I think JS-only devs prefer destructuring mostly because there is no discoverability with IntelliSense autocompletion when writing props. in JavaScript (...that doesn't have type annotations).

Having to write props.someLongThing more than 5 times in a lot of components, per day, and not have your IDE autocomplete it for you... I can see where point 2. starts making some sense... and that is why I don't like writing in programming languages that have no types or type annotations. In 2023 no programming language should be completely without types in my opinion (not saying dynamically typed languages shouldn't exist).

Summary & take-away

To me, everything related to how good code should look, comes down to just a few principal things like code cohesion, coupling, and in general, writing as much self-documenting, maintainable and easy-to-understand code as possible.

At its core, to me, this is about caring for your fellow developers who will have to maintain the code you wrote, and ultimately about caring about your craft, and, let's not forget, the project and its users.

Granted, there are cases where I would use destructuring myself but I rarely do, and it's mostly when I have small components with just one or two props.

So, I would advise you to write simple, obvious code. Don't try to be too "elegant", too abstract, or use every feature in the language to look smart. Think about how your code can express the intent one line at a time, in a cohesive manner, and with a clear separation of concerns, and you will write maintainable and easy-to-understand code.

Edit added on 05.09.2023

A comment reminded me of another example of why I often don't like destructuring when I see it:

interface MyComponentProps {
  a: string;
  b: string;
  c: string;
  d: string;
  e: string;
  onBChanged: () => void;
}

const MyComponent = ({ a, b, c, d, e, onBChanged }: MyComponentProps) => {
  // ...
};

This might look OK now, but it becomes unreadable after that destructured arguments list starts to be wrapped around:

const MyComponent = ({
  a,
  b,
  c,
  d,
  e,
  f,
  g,
  h,
  i,
  j,
  k,
  l,
  onBChanged,
}: MyComponentProps) => {
  // ...
};

And even worse when you have longer property names + a second-level destructuring adding another layer of curlies that destructure other objects. You could perform this second-level destructuring in the body, but then you're complicating the code even more! You have even more indirection now, with multiple goings back and forth between nesting levels, making it even harder to understand what is happening from a glance.

Instead, I prefer to at least make the function signature readable:

const MyComponent2 = (props: MyComponentProps) => {
  const { a, b, c, d, e, onBChanged } = props;
  // ...
};

This way I can glance over the component or function signature and immediately see what arguments it takes, and most of the time I can even deduce what the component or function is most likely doing (granted your project needs to have good names and a good naming scheme for types, for this to be effective).

But when I can't do that, then I go deeper with the refactoring and will most likely end up removing the destructuring altogether.

ย