Loosely coupling with Typescript
Tags: typescript
Published: 2023-10-16 16:20:59

---
title: Loosely coupling with Typescript
published: true
description: Adjusting slightly how you use types, you can get code that's easier to maintain, test and reason about.
tags: ["typescript"]
canonical_url: https://ivanalejandro0.com/blog/loosely-coupling-with-typescript
---

## Introduction
On this article we'll go through a simple example that shows how to type the data you pass around on your application in a way that makes it easier to test and maintain.

## About "loosely coupling"

> In computing and systems design, a loosely coupled system is one in which components are weakly associated with each other, and thus changes in one component least affect existence or performance of another component
>
> -- https://en.wikipedia.org/wiki/Loose_coupling

We'll adjust our types and code to connect our components more loosely.


## Example
Let's say we have a chat application, we are dealing with messages, and we have a utility function that calculates the size of a list of messages.

```typescript
// this usually lives on some other file and is imported here
interface MessageModel {
  id: string;
  author: { id: string; username: string };
  text: string;
  timestamp: number;
}

function getMessagesSize(messages: MessageModel[]): number {
  let size = 0;
  messages.forEach(message => size += message.text.length);
  return size;
}
```

We have the `MessageModel` interface defining the shape of our data, and then on the `getMessagesSize` function we receive a parameter of that type.

Simple enough, and it'll work just as expected. But as you add more features and functions, and your code grows, using this approach makes it harder to maintain over time than it needs to be.

## What's the problem?
The function we just wrote only needs to use the `text` field, but it "knows" (through the parameter type) about all the other fields that a message has. And not only knows, it **requires** the other fields to be there.

Why is that a problem? You may ask. Let's write a simple test for our function:

```typescript
test("that the messages size is correct", () => {
  const messages = [
    { text: "hello" },
    { text: "world" },
  ];

  const size = getMessagesSize(messages);
  expect(size).toBe(10);
});
```

You'll get this error:

```
error: TS2345 [ERROR]: Argument of type '{ text: string; }[]' is not assignable to parameter of type 'MessageModel[]'.
  Type '{ text: string; }' is missing the following properties from type 'MessageModel': id, author, timestamp
```

It basically says that you're passing the wrong type to the `getMessagesSize` function.

In order to make it right, you need to write the test like so:

```typescript
test("that the messages size is correct", () => {
  const messages = [
    {
      id: "test-message-id-01",
      author: { id: "test-uid-01", username: "username-01" },
      text: "hello",
      timestamp: 1234,
    },
    {
      id: "test-message-id-02",
      author: { id: "test-uid-02", username: "username-02" },
      text: "world",
      timestamp: 2345,
    },
  ];

  const size = getMessagesSize(messages);
  expect(size).toBe(10);
});
```

We have to write some "test data" just to construct a type that works with what `getMessagesSize` needs, when we only care about the `text` field.

Also, if the type changes in the future, we'll have to update our test, even if the change is irrelevant for what we want to test and for the implementation of the function we want to test. For example either adding or removing fields, that are not the `text` field that we use on this function.


## Different valid data shapes
Another problem we may face is that you may not be able to use the `getMessagesSize` function on different places of the app freely. A common use case is to use GraphQL APIs to get the data we need for the parts of the app we're working on. So most likely the `Message` will look different on other parts of the app.

For example, on a message list you may care about the user avatars, but on a notifications icon you may only care about the number of messages.


## How can we make this better?
Let's change how we declare the type of data we expect to receive on our function.

```typescript
interface Message {
  text: string;
}
function getMessagesSize(messages: Message[]): number {
  let size = 0;
  messages.forEach(message => size += message.text.length);
  return size;
}
```

Note that we don't need to change the actual data that we send to this function, we are just saying that the data we expect needs to have a `text` field.

This new function signature will accept any message that has a `text` field. The message could have any number of fields or just `text`.

Because of how types work[^1] any "extra" fields will be accepted with no complaints.

[^1]: If you're interested on how typescript matches types and data even if they are not exactly the same read about "structural typing" on the Typescript Handbook: https://www.typescriptlang.org/docs/handbook/type-compatibility.html.


Now we can write our test like so:
```typescript
test("that the messages size is correct", () => {
  const messages = [
    { text: 'hello' },
    { text: 'world' },
  ]
  const size = getMessagesSize(messages);
  expect(size).toBe(10);
});
```

Not only this is simpler, but if the type changes in the future, we won't have to update our test since we typed the `getMessagesSize` function with only what's relevant.


## Considerations

Note that since we are now using a parameter that only has what's needed for the implementation, if eventually we need to update the function to use other fields that we may be passing, we'll need to update the type/interface as well, even if the data we need is there, the shape we defined says otherwise, which in my opinion is a good trade-off since we are working with what we need.


## Playground

Here's the two code examples we discussed on the article, loaded on a Typescript playground for you to experiment:

* The full `MessageModel` and the function requiring it: [playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIQM4bgcwqgewBMIAbZAbwChllgiAuZDMKUHAbhuTgFcwAFgShMKdRs1bsOyXhmgg4AWwhMWbEDmQBfLrUgAPMGqma9yMMBUtlAByYheSgEbQu2qlRi8QCSwRBkPDB0LFxMAGVgAC8IAAprbDwMJlCk-GIyAG0AXQBKBydXKEpuUggwZhiUAF5kAAZzRPCMADoYYQBRRAEEzHTkGoA+KtjkAGo65rxWwzBW8s1BPPMoCt4oQIxq908EAJZkacxB5CzualpaeiYAIkgWAFolekf6gEZbgBpuWj5BYSicR3B5gR68V4fb6yeSbZSqZC3OQKeFvT46H5XCwQIx3ARkUgEb6-CxWTBgOxMd4AJgAzAAWTEYi4km6I0HPSHU4lY-5CESUYHs8ngrnQ5FwlR3CWKFRvbnMrFzO4Ad2EpCIPKulmsFKU9mQdPpAFYmdofjk9gcCOUFgQcHFbmkWqNVNDgs7klFYn0wsk8nkgA)
* And the function requiring only the `text` field: [playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIQM4bgcxQbwChllIAPMALmQzClBwG5CBfQwmAVxATGAHsQyPGHRZcmAMrAAXhAAUAW0zY8GamNUQA2gF0AlNRCdFAI2jIiJADYQwNWSgC8yAAzMSy8WoB0MflAAoogAFkoqEshOAHwOcsgA1C5eWj7kYD62IDhgIfoeyFB2nFBCGI7MbIQA9NXC-PwAJsiNJooAni1wYHDICHAgjcCN3Sj+UMjcwPaQtBiECIK0yCkSGFHI2sSWpBAU1ADkIRDW1vwHyCwANNv4u-vIBwDuAdaNF9eEuuyLIBj8tky-Bw8gARJo1nEIJRQVdhHYIWppHJwt5MPp9EA)