TypeScript | Organizing and Storing Types and Interfaces

Are you are new to TypeScript and want to start working on your first project? Some of the questions many developers ask are: How do you structure your types in a TypeScript project? What is the best way to structure your typings that is robust and expandable? Where to put interfaces and type aliases? and other similar questions. In this article, I hope to give you some ideas.

One of the things that come to mind when I see these kinds of questions is: It seems everyone wants to get it right the first time. In fact, I think about those things when starting a new project as well. Throughout my experience working on several different projects, small-custom apps, large-government apps, and product-based apps, and personal projects, etc. I’ve realized a common pattern regardless of how different projects are structure.

The pattern is: Developers use the same folder structures used by their company. This means I’m working for company X developing app A, and in a few weeks I will start working on app B for the same company X, chances are I will use the same organization structure used to develop app A for app B. Unless there were major issues faced when using organization structure for app A, there will be no major changes in app B.

For those looking to use the best project organization structure, whether it is TypeScript, JavaScript, C#, GoLang, Python, etc, there is no best way to structure a project. Every project is unique and has its own challenge. Hence, its organization will be highly dependent on frameworks and architectural patterns used for the project.

This applies also when organizing and storing types and interfaces in a TypeScript project. For example, if your project has a backend and a frontend, chances are you are using two different frameworks for each. Each framework tends to model the organization of the project as generally, frameworks used an architectural pattern. For example, using Express.js on the backend and Next.js on the frontend can make your project’s structure shift in two different directions.

In fact, you will see differences when using two different frontend libraries or frameworks such as React and Angular. One of the best ways to understand solid project organization structure is by working on different projects, with different developers, and different companies.

Unfortunately, not every developer has the chance to work on different projects using different patterns and frameworks. That’s why I’ll show you some alternatives you could use to start organizing and storing types and interfaces in your TypeScript project.

Global-based

One way to manage your types and interfaces is by storing them all in on unique types.ts file folder, typically located at the root of your project, such as src/types.ts or src/compiler/types.ts. This could look something like this:

src 
|--
  |-- components
  |-- services
  |-- routes
  |-- global
       |-- types.ts

In this example, you will see all of your types, interfaces, enums, classes, etc in one single types.ts file.

This approach is generally used to simplify the process of locating types. Every single member of the team will know types will be stored in one place and failing to do so will quickly create inconsistencies in the organization of the project. This works for small and large projects as it serves as the source of truth.

One disadvantage of using the global-based organizational structure is the number of lines in the types.ts becomes proportional to the project size. If the project is small, there is a small number of type, interface, or class definitions. The same way happens if the project is large, it should not be a surprise to see 1000+ lines of type definition in the types.ts file.

When creating new functions, new services, or new models, there’s always some level of type definition involved. Therefore, if you have multiple developers working on a project, merging conflicts will be a practice the team will encounter as the types.ts file is constantly changing.

Another problem you could face with this structure is that you cannot have different variations of a type or interface without coming up with multiple similar names. What do I mean by that? For example, we could have a car.ts file using a Car interface to manipulate a Car object to perform CRUD operations.

interface Car {
  id: string;
  year: number;
  brand: string;
  model: string;
  style: string;
}

Later on, we create another file called user.ts. There, we manipulate all user-related information. We can also pull information about a user’s cars. We might want to access only the brand and model of the user’s cars. Typically, we would use the Car interface to define the User interface to show an array of cars.

interface User {
  id: string;
  firstName: string;
  lastName: string;
  dob: Date;
  cars: Car[];
}

At first, we might be able to use the Car interface as brand and model are properties of the Car interface. However, if I decide to include additional custom information in the cars of a user, the Car interface would change. However, we don’t want to fully change the Car interface as that shows how the necessary car object properties to perform CRUD operations. Therefore, you end up creating another CarUser interface.

interface CarUser {
  brand: string;
  model: string;
  color: string;
}

interface User {
  id: string;
  firstName: string;
  lastName: string;
  dob: Date;
  cars: CarUser[];
}

Now, we have two similar interface names, Car and CarUser, in the same types.ts file. This can quickly lead to confusion when bringing on new developers to a project and adding more similar interfaces with similar naming to illustrate correctly an object.

Don’t Confuse with Global Scope

Don’t confuse the term global used in this article. Generally, when we use the term global, we think of scope. Therefore, if we refer to a global-scoped type or interface, it is assumed we can use the type or interface in any file of the project without the need of importing (import) them from their file where they are defined.

Component-based

With the major progress made in the way the frontend is developed and the libraries and frameworks available, component-based development has become the norm for frontend development practices. In the same way, the file organization of a project resembles that component-based architecture.

If you have used Angular in the past, you can quickly start a project using its cli.

ng new

Once Angular CLI creates a new project, the organization will look like the following:

src
|--
  |--app
      |-- app.component.css
      |-- app.component.html
      |-- app.component.spec.ts
      |-- app.component.ts
      |-- app.module.ts
  |--assets
  |--environments

From the start, Angular is showing you the app folder is a component. This is easy to recognize as it is included in the name of the files inside the app folder. Everything needed to run a component is also there: HTML, CSS, the code logic (inside the app.component.ts file), and test cases (app.component.spec.ts).

In the same way, you could add type definitions in the component. For example, if our project has several components:

src
|--
  |--app
  |--components
      |--car
      |--user
      |--auth
  |--assets
  |--environments

We could have a .types.ts or .model.ts file inside each component.

src
|--
  |--app
  |--components
      |--car
        |-- car.component.css
        |-- car.component.types.ts
        |-- car.component.html
        |-- car.component.spec.ts
        |-- car.component.ts
        |-- car.module.ts

In this way, you could add all the types used in a component. In our case, the car component. The good part about this is you will prevent confusion when using similar types in different components.

If we look again at the case of having a Car interface used primarily in the car component, but also a User interface used in the user.component, the User could have an array of cars. These car objects could look different from car objects used in the car component. Since each component has its own types file (car.component.types.ts and user.component.types.ts), we could generate Car interfaces on each component with property attributes that best fit the use case on each component.

One disadvantage with this approach is to have duplicate interfaces. In the case we have a new component called brand, we could have the scenario of pulling all the cars that a brand can produce. An interface similar to the one defined in the car.component.type.ts file could be what we need for brand.component.type.ts. However, we will end up creating a duplicate definition of the Car interface in the brand component.

Model-based or Object-based

Another way to think about where to locate types and interfaces is by defining the models used throughout the codebase. If you are not familiar with the term “model”, the model is generally used to represent the structure of a record in a database table. If a table called users have the columns: id, first_name, last_name, dob (date of birth), height, weight, etc), the interface model would look like the similar:

interface User {
   id: string;
   firstName: string;
   lastName: string;
   dob: Date;
   height: number;
   weight: number;
}

Therefore, we could use that model in different parts of the application, frontend or backend.

In the case of the frontend, we could have multiple components using the User interface such as the auth, user, and even car component. Also, we could use the same model regardless of whether the component is a parent/feature component or a child/presentational component.

src
|--
  |--models
      |-- car.model.ts
      |-- user.model.ts
      |-- brand.model.ts

In the case of the backend, we could use the model interface in multiple controllers and services, allowing us to understand what database table record it is used in different code logic and their relation with a certain process and/For functionality.

Typed-based

Another approach that could be used is the typed-based approach. The way this works is by storing enums, types, interfaces, classes, in their own files.

src
|--
  |--ts
     |--enums
     |   |-- a.enums.ts
     |   |-- b.enums.ts
     |--interfaces
     |   |-- a.interfaces.ts
     |   |-- b.interfaces.ts
     |   |-- c.interfaces.ts
     |   |-- d.interfaces.ts
     |--types
         |-- a.types.ts
         |-- b.types.ts

I see this approach as an organized global structure as all of the types, interfaces, enums, classes will be located in a central/global folder and categorized in different subfolders.

Hybrid

The hybrid approach uses a global and component or model-based approach. In that way, you can store types and interfaces in a global folder that are common across the project. Also, you can store specific types and interfaces that are strictly related to a specific component, controller or service.

Experimentational-based

At the end of the day, there is not a golden rule of which organization structure to use. Projects are different and they have their own uniqueness.

For example, we might be working in a React project and have class or functional components accepting different props or properties. If you are not familiar with props in React, they allow us to pass information to a component using props. For those Angular developers reading, you can think of it as using @Input and @Output decorators to share data between components. We could define in the same component an interface for the props of that component.

interface PropsType {
  children: JSX.Element
  name: string
}

class Component extends React.Component<PropsType, {}> {
  render() {
    return (
      <h2>
        {this.props.children}
      </h2>
    )
  }
}

You can organize the types based on your development experiences from different projects, or if you are brand new and starting out your first project, you can follow your intuition and organize the types in the way it makes the most sense to you. Only until you write more and more code and your applications start growing, and so your team, you will see what organizational structures that fit best for each type of project and even each stage of a project.

Share this article or checkout our social media channels!