Repeat the Code, Not the Information

Tomasz GilTomasz Gil
6 min read

The past year has been pivotal for me and my views on repeated code. "Don't repeat yourself" (DRY) is seen as a good practice, and rightly so. However, like any good practice, it needs context to be applied correctly. Without this context, applying a good practice can often be harmful.

I find that engineers often rush to create abstractions and reuse code, trying to avoid duplication at almost any cost. There's a major problem with this approach. Just because code looks similar or even identical doesn't mean it should be shared. It might represent fundamentally different information and, as a result, evolve in different directions.

This article is another attempt to explain this concept—from the perspective of types.

When types should be shared

You might have a situation where you have several different but related components. Maybe they operate at the same level or are used together. They share the same props.

Let’s imagine we have a feed showing chat messages. Each message has its own error handling and a preview for extra media content. We use a small set of components to build the feed. We need to pass the id of the message along with it’s content and the threadId to which the message belongs. All these components also use the current browser location inside their implementation.

Altogether, they all require the same props.

// Example #1 - Chat Messages

const ChatMessage = ({ id, threadId, location, data }) => {}
const ErrorFallback = ({ id, threadId, location, data }) => {}
const MessagePreview = ({ id, threadId, location, data }) => {}

Here’s the main question I’ll be exploring in this article.

When we have multiple components with the same props — should we create a common type definition for props or one for each component?

Technically, we don't need separate interfaces if all components have the same props. However, I recommend having separate interfaces for different components unless the intention is for them to share the same interface.

Here's what I mean by this.

Let’s start with a single prop

Let's consider a different and much simpler example. Imagine we have three components, all in one file.

// Example #2 - UI Components

const Button = ({ children }: ButtonProps) => {}
const Alert = ({ children }: AlertProps) => {}
const Dialog = ({ children }: DialogProps) => {}

How should we define the interfaces for these components? They all have the same props—should we create a single Props interface? They only accept children, so it seems like a good opportunity to reuse code. DRY, right?

You might feel that this approach is not quite right. Technically, it wouldn't be incorrect, since the code to write an interface for each component is exactly the same. However, it would be wrong semantically—a button is not an alert, and an alert is not a dialog. The interfaces are not truly the same, even if they appear to be identical.

Let’s add the second prop

Let's expand on our example. Ask yourself, how are these components most likely to change over time? As time goes on, we might want to add more props. I asked AI, and it suggested these changes as the next most likely to occur:

// Example #2 - UI Components

const Button = ({ children, onClick }: ButtonProps) => {}
const Alert = ({ children, status }: AlertProps) => {}
const Dialog = ({ children, isOpen }: DialogProps) => {}

Each component received a new prop, and each prop is unique to the component. As expected, they all evolved differently. If we had used a single Props interface, we would have needed to return to separate interfaces because, fundamentally, they were never a single interface. Each interface carried different information.

To sum it up in one rule: repeat the code, but not the information.

Returning to our initial chat message example—since these three components have different purposes, serve different functions, and carry different information, it's better to declare separate prop interfaces for each component. If they accept the same props, those individual props should share a type.

// Example #1 - Chat Messages

interface ChatMessageProps {
  id: string;
  threadId: string;
  location: string;
  data: Message;
}

const ChatMessage = ({ id, threadId, location, data }: ChatMessageProps) => {}

// We're creating an interface per component...
interface ErrorFallbackProps {
  id: string;
  threadId: string;
  location: string;
  // ...and sharing the type for individual props.
  data: Message;
}

const ErrorFallback = ({ id, threadId, location, data }: ErrorFallbackProps) => {}

interface MessagePreviewProps {
  // Same content as in ChatMessageProps and ErrorFallbackProps
}

const MessagePreview = ({ id, threadId, location, data }: MessagePreviewProps) => {}

Horizontal vs. vertical slicing

There's an important point to consider. Here, I'm looking at the repeated information from the perspective of the domain. The vertical separation of concerns. You might argue that if you view this information from a technical level (horizontal), such a reusable type could make sense. We can create a generic type to capture this idea.

type PropsWithChildren<Props extends object> = {
  children: React.ReactNode;
} & Props;

React even had this at the component type level—React.FC. This type still exists, but it no longer automatically includes children in the props.

It might be useful to have such a type just for utility, especially in lower-level code where there's no business domain, like in your framework. However, when you start incorporating business, design or user experience decisions, I think it's better in the long run to avoid splitting the information horizontally. This approach allows for a clear separation without too many layers of abstraction.

Diagram showing properties of Button, Alert, and Dialog components.

The implications of sharing interfaces

Let's look at a different example where creating a shared interface is sensible.

// Example #3 - Error Fallback Components

interface ErrorProps {
  title: string;
  message: string;
  error: Error;
}

const DefaultError = ({ title, message, error }: ErrorProps) => {}
const NetworkError = ({ title, message, error }: ErrorProps) => {}
const TypeError = ({ title, message, error }: ErrorProps) => {}

We also have components that accept the same props, but this time they are using a common interface. This approach communicates an important message: these components share the same requirements and are likely to evolve together in the future. Of course, this might not always happen, but at least there's a clear intention behind it.

Let’s consider the changes we might introduce to these components. If we want to control an illustration displayed for an error, we probably want to apply the change to all components. Similarly, if we want to add retry functionality, we likely want to apply the change to all components.

You see my point—sharing an interface actually means that these components have a common interface. They are the same now and likely will need to be the same in the future.

Conclusion

“Don’t repeat yourself” is a more nuanced concept than we might realize. It’s not just about repeated code; it’s about repeated information. The first is easy to notice, but the second is incredibly difficult to spot. My approach is to let patterns emerge, and when it becomes clear that something should be abstracted, then abstract it. Doing it the other way around can be quite painful.

Overall, here's the mental model I find useful when thinking about components that accept similar props.

  • Components that are related and used interchangeably should share the same interface.

  • Components that are unique and specialized should not share the same interface because they serve different purposes.

If you enjoyed the article or have a question, feel free to reach out to me on Bluesky or leave a comment here! 👋

Further reading and references:

0
Subscribe to my newsletter

Read articles from Tomasz Gil directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tomasz Gil
Tomasz Gil

I help product teams build quality software and lead engineering efforts. Currently working at Salesloft as a Senior UI Engineer.