paint-brush
Using Destructuring and Inline Types Can Hurt Your TypeScript Codebaseby@baransu
1,271 reads
1,271 reads

Using Destructuring and Inline Types Can Hurt Your TypeScript Codebase

by Tomasz CichocińskiOctober 10th, 2022
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

Using destructuring and inline types makes your TypeScript less readable. I want to show you how using destructuring makes it less readable in TypeScript. It discourages creating smaller helper functions and relying on composition to build up your main function logic. There is no natural place to write documentation comments when all the types are cramped with destructuring in the function definition. It's just a lot of space of code and takes a lot-of-space of lines of code. In addition, it focuses on implementation detail in addition to the implementation detail.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Using Destructuring and Inline Types Can Hurt Your TypeScript Codebase
Tomasz Cichociński HackerNoon profile picture


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


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


What does the 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
};


When function definition is quite simple, the function accepts a few parameters 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
}


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
}


Why do 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, 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 a 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
}


Resources

Find a list of resources I used when researching this blog post: