DEV Community

Tomasz Cichocinski
Tomasz Cichocinski

Posted on • Edited on • Originally published at cichocinski.dev

5 reasons why destructuring and inline types hurt your TypeScript codebase

Recently I've seen a tweet by Jamie Kyle about using destructuring, default params, and inline types:

That tweet and a few React components I have seen recently in my day job inspired me to write this blog post. I want to show you how using destructuring and inline types makes your TypeScript less readable!

How TypeScript function definition look like?

In JavaScript and TypeScript, you can define a function either using function keyword or lambda/arrow-function. Both ways are valid, but have their differences. Let's take a look at the simple sendMessage function. Implementation logic is not relevant to us.

// sendMessage function written using `function` keyword
function sendMessage(message: string) {
  // function logic
}

// same sendMessage written as arrow function
const sendMessage = (message: string) => {
  // function logic
};
Enter fullscreen mode Exit fullscreen mode

When function definition is quite simple, the function accepts a few params of a different type. If they are primitives like strings or numbers, everything is readable.

Let's say you want to pass some additional information alongside your message content to the sendMessage function.

function sendMessage(message: {
  content: string;
  senderId: string;
  replyTo?: string;
}) {
  // you can assess content using `message.content` here
}
Enter fullscreen mode Exit fullscreen mode

As you can see, TypeScript allows you to write an inline type definition for the message object you want to pass without specifying the type using type or interface keyword.

Let's add destructuring. When you pass a large message object to your function, TypeScript allows breaking apart passed arguments to reduce the code boilerplate of repeating message variable many times.

function sendMessage({
  content,
  senderId,
  replyTo,
}: {
  content: string;
  senderId: string;
  replyTo?: string;
}) {
  // you have access to `content` directly
}
Enter fullscreen mode Exit fullscreen mode

Why I think it's a bad idea and how you can make it better

It may look like a nice idea, after all, you don't need to write message that many times, right? It turns out it's not that great. Let's talk about 5 reasons why I think it's an antipattern.

1. You're not sure where your data is coming from

When you're reading the function body, and you see senderId you have to double-check to be sure from where that function comes. Is it passed as an argument or calculated somewhere in the function?

2. It's hard to write documentation

There is no natural place to write documentation comments when all the types are cramped with destructuring in the function definition. You could write comments between each type field, but that makes the whole function definition even longer. It's actively discouraging you from writing a quick summary of the data you're passing.

3. It's hard to pass that data forward

When your data is destructured you need to structure it again into a new object if you want to pass it forward. This discourages creating smaller helper functions and relying on composition to build up your main function logic.

4. You cannot reuse argument types outside this function

If you need to reuse your function arguments in helper functions when composing your main function logic, you must type the same set of types repeatedly. This makes it easier not to write types at all.

5. It just takes a lot of space

Let's face it. It's just a lot of lines of code that take up a lot of screen space. And in addition, it focuses on implementation detail – the inner type of the arguments you are passing to a function, which is most of the time not relevant when you're looking at that function.

Just create type for it

Extracting the type and placing it right above the function makes it much more readable. There is a place for documentation comments, you can reuse that type in some other helper function and change the type definition in one place if needed.

/**
 * Message to send using XYZ API
 */
export type MessageToSend = {
  /**
   * Markdown string of the user's message
   */
  content: string;
  /**
   * Id of the sender user
   */
  senderId: string;
  /**
   * Other message ID if this is a reply
   */
  replyTo?: string;
};

function sendMessage(message: MessageToSend) {
  // function logic
}

function getUserIdsToNotify(message: MessaageToSend) {
  // function logic
}
Enter fullscreen mode Exit fullscreen mode

Resources

List of resources I used when researching this blog post:


Read more at cichocinski.dev

Top comments (0)