10/10

Article thumbnail

🌟 Concerns about separating types from implementation

7m
quality
patterns
development

Intro

We'll check how separating types from implementation impacts our applications from different perspectives.

The topic introduction

I've seen a lot of different opinions about splitting out the type definitions from run time code (our application JavaScript code). A lot of people prefer to have types in the same file - just before the implementation.

Loading

At the same time, other developers prefer to create files for types per each module.

Loading

I've my own thoughts about this topic and it's a good time to share them and summarize what I've learned during some research and after trying some stuff in code.

Today we'll understand the pros/cons of splitting type definitions to separate files and we'll check when it's worth to use each option.

Little spoiler - as usual in programming it depends from many factors 💨. Continue reading to spot them.

Good, old and universal rule - the separation of concerns

When you're creating a big feature you probably separate your codebase into distinct sections. The sections will be different based on the platform or technology that you working with. In languages like C#, it's common to separate interfaces (shape of the program/contract), to other files to achieve reusability, modularity, maintainability, and easy-to-read/understand codebase. This is a universal rule that can be applied to any technology.

Let's take a look at an example in which we'll implement complex button component with motives, variants, and other stuff that are usually used in web apps.

Loading

Now it's time for type definitions - at this moment we'll place them in a separate file.

Loading

Lastly, let's add some constants.

Loading

We're humans, and still, we maintain the codebase (not AI 💔), so it's good to be able to work with code faster and in a more productive way. This simple example shows, how quickly simple component code can increase its number of lines and can start to be harder to read.

Compare now what we had with this solution in which everything is added in a single file.

Loading

Imagine now holding type definitions for the big library in the same files with implementation 💥.

If you want to check separation of concerns in practice, feel free to visit this repository link with the above-mentioned example.

The circular dependency risk

Circular dependency is a situation when module A will import something from module B and vice-versa.

Loading

Unfortunately, if you store the type definitions in the same file with implementation, you increase the risk of providing circular dependency, and with that you can increase a application bundle size, or you can provide really weird/hard to spot bugs.

The real-world example can be spotted in Redux setup boilerplate.

Loading

Firstly, it looks really weird (for me of course). Type declarations are hoisted, but anyway, it looks like we're using two different languages at the same time. It makes it harder to spot what is going on in this file.

Secondly, imagine that we want to create our custom middleware that listens for dispatched actions, and if it contains pending in action type we want to log the payload of an action. The middleware type from redux takes a type of state to be passed.

Loading

If we want to attach this middleware to our store, we need to import it and pass it to a configureStore function.

Loading

As you saw creating circular dependency is really easy. We can avoid such a situation by storing type definitions in a separate file.

Loading

Now let's change in both places import statements.

Loading

Lastly, little change in the middleware file.

Loading

So, as you saw storing type defs in separate files reduces the risk of having circular dependency between modules.

There are tools that allows us to track circular dependencies. One of them is the nx. If you are interested, read the Overview of the main functionalities of the NX tool and Dream stack for React dev articles.

The impact on performance

Rebuild time 👍

When you modify a TypeScript file, the TypeScript compiler needs to recompile that file and any files that depend on it. If your type definitions are centralized in a single file and many other files depend on it, a change to that central file can trigger the recompilation of a large portion of your codebase.

In contrast, if type definitions are separated into smaller, more focused files, changes to one type file are less likely to trigger unnecessary recompilations in unrelated parts of the codebase.

Parallelism 👍

When type definitions are in separate files, the build tool can parallelize the compilation of these files, potentially reducing the overall build time.

Initial build time

If you have an excessive number of small type definition files, the initial load time for your development environment (e.g., in an IDE) might be longer because it needs to load and parse more files.

Note about performance

Before you start any changes in your codebase it's good to have consistent measurements of build times. You have tons of options, but I think the best one is just creating a small script that will create a file in each build (on pull request) with a snapshot of the time required to build dedicated application or module.

Don't try to fix problems that don't exist!

Modeling aspect - types first approach

Before you start to work on any code, you should try to think about the shape of API, structure and dependencies. By splitting types from implementation, you are doing the modeling process - types first approach.

Because of that your API will be more consistent and you'll be able to prepare multiple implementations. They will be independent, but still will have the same shape. Later, it's much easier to choose the best one or replace the other if it contains a bug/has dramatically bad performance.

Look at following implementation of multiple variants of sum function, with separated contract:

Loading

The implementation process of multiple sum variants

The use cases for each option

The general rule is: "Split your types if you need them in other places".

Application core, domain models

Loading

Reusable, presentational components

Loading

Multiple variants of something

Loading

Variants are great use case for separate types

Modeling state and actions

Loading

Reusable logic

Loading

Standalone libraries

Loading

Avoid doing split if you don't need types in other places.

If you have concerns about which option to use in the dedicated scenario, inspire yourself with the following repository.

Let's summarize what we learned

Topics like that are always hard because they cannot be explained in a 0/1 way. So what we have learned today?

Simple split may increase code readability, especially if your type definitions are large.

Splitting types into separate files may remove problems with circular dependencies in your code base, but this problem occurs in really rare situations, so it's not a big deal.

Types in separate files will reduce the code rebuild time due to parallelism and caching, but may increase the initial build time.

We shouldn't do any premature optimizations - measure first and then try to improve build times (if it is a real problem!).

Splitting out type definitions allows us to design a nice and developer-friendly API, and it's part of the software modeling process that may have a positive impact on code quality.

It's worth splitting types for domain models, libraries, reusable/presentation components, when you want to have multiple variants of implementation, but when you developing small, internal code, it's better to keep types in the same file - there is no need to reuse them in other places.

So if you ask me - should I separate definitions from implementation? If it will be a C# ecosystem I would say - definitely yes, but it's TypeScript, and my answer is: do it if you are writing a separate library, creating reusable components or you're modeling your application domain, but for other cases, I think it's not needed...

Don't try to follow all programming rules like religion.

I create content regularly!

I hope you found my post interesting. If so, maybe you'll take a peek at my LinkedIn, where I publish posts daily.

Comments

Add your honest opinion about this article and help us improve the content.

10/10

created: 06-09-2023
updated: 06-09-2023