Refactoring complex types

Structure the types of your application in a more readable and maintainable way

In TypeScript sometimes we come across complex types that we wish we could work with them in an easier way, I’ll give you the following example

enum VehicleEnum {
  CAR = "car",
  MOTORCYCLE = "motorcycle",
  BICYCLE = "bicycle",
}
type VehicleProperties =
  | "self-powered"
  | "two-wheeled"
  | "four-wheeled"
  | "a-license-required"
  | "family-friendly"
  | "other";
const vehicles: {
  [key in VehicleEnum]: VehicleProperties[];
} = {
  [VehicleEnum.CAR]: [
    "self-powered",
    "four-wheeled",
    "a-license-required",
    "family-friendly",
  ],
  [VehicleEnum.MOTORCYCLE]: ["two-wheeled", "a-license-required"],
  [VehicleEnum.BICYCLE]: ["two-wheeled", "a-license-required", "other"],
};

In which is very difficult to see the properties of each vehicle, you will have to search through each of them to find a vehicle that is self-powered for example:

const findSelfPoweredVehicles = (vehicles: {
  [key in VehicleEnum]: VehicleProperties[];
}) => {
  return Object.keys(vehicles).filter((key) =>
    vehicles[key].includes("self-powered"),
  );
};

So let’s find a better way to define all these types of vehicles. Let’s start with the vehicle object

type VehicleName = "Car" | "Motorcycle" | "Bicycle";
type Vehicle = {
  name: VehicleName;
  properties: VehicleProperties[];
};

const vehicles2: Vehicle[] = [
  {
    name: "Car",
    properties: [
      "self-powered",
      "four-wheeled",
      "a-license-required",
      "family-friendly",
    ],
  },
  {
    name: "Motorcycle",
    properties: ["two-wheeled", "a-license-required"],
  },
  {
    name: "Bicycle",
    properties: ["two-wheeled", "a-license-required", "other"],
  },
];

This way we can easily find a self-powered vehicle:

const findSelfPoweredVehicles = (vehicles: {
  [key in VehicleEnum]: VehicleProperties[];
}) => {
  return Object.keys(vehicles).filter((key) =>
    vehicles[key].includes("self-powered"),
  );
};

Have a look at this function, we don’t have the Object.keys, we don’t have to use the key to index the object again and we can easily define the vehicles passed to it as just Vehicle[]. So why is this better you might ask, is just a function that does one thing, well, let’s look at the point where we add more data to this system:

  • We want to track the miles per gallon or liters per 100km.
  • We want to track the weight of the bicycles, if they’re road or mountain bikes.
  • We want to know if there’s space in the back for a baby car seat
  • Etc.

If we keep using the first structure things can get very ugly very quickly as we always need to loop through each element of the enum and transform it to an object to find a single property. But if we use the second structure we can easily re-define the vehicles types by classifying between self-powered and non-powered vehicles:


type VehicleName = "Car" | "Motorcycle" | "Bicycle";
type Vehicle = {
  name: VehicleName;
  properties: VehicleProperties[];
};

type NonPoweredVehicle = Vehicle & {
  isSelfPowered: false;
};
type SelfPoweredVehicle = Vehicle & {
  isSelfPowered: true;
};

const motorizedVehicles: SelfPoweredVehicle[] = [
  {
    name: "Car",
    properties: [
      "self-powered",
      "four-wheeled",
      "a-license-required",
      "family-friendly",
    ],
    isSelfPowered: true,
  },
  {
    name: "Motorcycle",
    properties: ["self-powered", "two-wheeled", "a-license-required"],
    isSelfPowered: true,
  },
];

const nonMotorizedVehicles: NonPoweredVehicle[] = [
  {
    name: "Bicycle",
    properties: ["two-wheeled", "a-license-required", "other"],
    isSelfPowered: false,
  },
];

const vehicles2: Vehicle[] = [...motorizedVehicles, ...nonMotorizedVehicles];

This way we don’t even need a function to find the self-powered vehicles and we use a nice inheritance structure in our types. And we can do this kind of structure for any type of property: “is-eco-friendly”, “allowed-on-highways”, “needs-parking-space”...

In my experience having a more complex types structure doesn’t make things more complicated and usually much easier to work with, having a slightly bigger types file is not an inconvenience at all and doesn’t make the system less efficient.

This kind of structure has proven to be very useful to me over the years when I want to work with back-end data that can come in any structure, we might receive a complex JSON with all these properties together and we can break it down into a better structure this way, making it more readable and most importantly more robust, you always work with types that have a clear interface, instead of just having an array of strings that can be anything.

Another thing that I want to point out is that getting rid of Maps when they’re not needed is generally a good idea for readability and simplicity. Maps are useful only when working with large data structures due to them being ordered and their just-in-time execution. Here is a good example on the difference between both of them. If you need to manipulate properties of each element then Maps is a better choice, but for our example we don’t need to change anything.

Conclusion, if you have a set of data that you know is not going to exponentially grow in the future and would like a better structure in your application then this refactoring is good for you.