TypeScript - Structuring type dependencies in frontend applications

This post is part of an ongoing series on TypeScript. In it, I try to explain in a detailed way different situations I’ve faced while building relatively big React applications using TypeScript, and moving them from JavaScript. Here’s a list of other posts I’ve written about TypeScript:

In your frontend applications, you may have types that represent a resource that stays the same regardless of what part of the code it’s used in. They may be part of your domain model, and they can grow pretty fat. For lack of a better word, let’s call them big types.

In practice, they may come from generated code if using tools like swagger-codegen or GraphQL code generation utilities like the Apollo CLI or GraphQL Code Generator. They may also be hand-written type definition files that represent response types from APIs you use.

In this post I’ll try to explain why although tempting, it’s a bad idea to use big types across your application code. A simple example could be a tool for finding and comparing hotels. In it, there are many big types such as the Hotel type, which contains a ton of information about each hotel. There may also be an interface for a Room or a Booking.

Let’s start with a visual representation of what I’m trying to stop you from doing:

├── types
│   └── api.ts
└── views
    ├── booking
    │   ├── Booking.spec.tsx
    │   └── Booking.tsx
    ├── hotel
    │   ├── Hotel.spec.tsx
    │   ├── Hotel.tsx
    │   ├── HotelDescription.tsx
    │   ├── HotelRooms.tsx
    │   └── room
    │       ├── Room.spec.tsx
    │       └── Room.tsx
    └── search
        ├── Search.spec.tsx
        └── Search.tsx

In types/api.ts, you have complete interfaces for what your API responds with when you request a Booking, a Hotel, and when you request and interact with a Booking. You may also have types like BookingCreateInput and SearchParameter.

What could go wrong?

Code ergonomics #

When the type changes, it’s very likely that you need to change a lot of code to go with that change as well. While the introduction of a new field in a type will likely not require changes in your application code, a breaking change on a big type could start a refactoring chain. The fact that the type is used indiscriminately everywhere makes it hard to see where logic should change.

In the case of generated types, a change in the code generation library will likely result in a situation similar to the one described above as well.

If you unit-test a lot, you’re going to need to mock a lot more things in tests for them to compile. This can make your unit tests a lot less clear and remove them from their purpose.

Proper structure and architecture #

You may stop thinking about the design of your components’ API and just use the type instead. Using a big type directly instead of thinking of what your component’s API should be can lead to code that’s harder to maintain, review and fix.

Transformations to the underlying data necessary for a component may be done repeatedly in different components that use the same type as a parameter. This could affect performance, but most importantly, it could make other developers rewrite code that’s already somewhere else.

Subtly, using big types directly instead of thinking of a lean API for your components also goes against layering of your application’s concerns. If you want to separate the presentation layer from the domain and data layers, using a type that represents your data in the presentation layer is likely to introduce coupling.

An alternative #

In the example above, the Hotel is a dependency of six components. The Room type is a dependency of four components. In a more realistic application, this number is usually a lot bigger.

A good refactoring goal is to reduce the number of touchpoints between your big types and your components while still keeping type safety. There are many ways to do this, but for example:

Another is to make your components more flexible:

Using big types in your presentational components is tempting because it’s a quick solution. It also makes logical sense. You want to render a Hotel and you have a type called Hotel, why not just use that? In addition to that, writing more types by hand to represent your interfaces may seem like a waste of time if the “real” type is already somewhere in your code. Hopefully, this post helps convince you otherwise.