How I ease the next developer reading my code
Tags: typescript, xstate, react, cypress
Published: 2022-02-23 16:45:18

Recently, I jumped on the codebase of a small product of ours (an integration with Shopify) to implement some last-minute changes before going live. I had an overall idea of what the product does, and the external team that worked on it received:

* comprehensive documentation of our coding patterns (they are publicly available in the [“WorkWave RouteManager UI coding patterns” article](https://dev.to/noriste/routemanager-ui-coding-patterns-generic-ones-4iaa))

* thorough code reviews by our front-end architect (you can get an idea of how vital code reviews are for us in my [“Support the Reviewers with detailed Pull Request descriptions” article](https://dev.to/noriste/support-the-reviewers-with-detailed-pull-request-descriptions-2khn))

Despite that, being effective since the first day was hard for me. Why? Because **four of the most important details that get a codebase immediately readable** were missing, which are:

1. **TypeScript’ Discriminated Unions** instead of optional properties (helpful to describe the domain)

2. **Straightforward JSX code** (ease reading and jumping through the code)

3. **Explicit State Machines** (describe what the app does from an internal perspective)

4. **Cypress integration tests** (tell what the app does from an external perspective)

I will explain why I weigh these four features in the following chapters.



## 1. TypeScript Discriminated Unions

Discriminated Unions (here is the [link to the TypeScript documentation](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions)) are a potent tool to express **why an object contains or does not have some properties**. When applied to domain entities, they act as quick documentation that tells the readers about the domain entity they are dealing with. Here is an example

```ts
type Order = {
  status: string
  name: string
  description?: string
  at?: Location
  expectedDelivery?: Date
  deliveredOn?: Date
}
```

*[link to the TS playground](https://www.typescriptlang.org/play?#code/FAFwngDgpgBA8gJwCZQTAvDA3sGMDOIAhiAK74BcBICAlgHYDmuM9RAtlFYXUyyvgDGdCCFoB7egH5uNBszwkZMADLjBJCfRZQAHtEEgoSACJQANrQBuqMMpMko-C9dTG40qg6PAAvsGAAM1J6Qy0YYShHAFF2IlpzAFkofHwiRigACnFkVCpEFAQASmwWWkCYbNyEADoSEpw8PAQoMgR6GAADABIsHMKatk5fGFp8GBIYXv7UOpBfTpZ-PHLKmdq9AyNTFxsEMAaWZtbSdq7p6sGOKBGAdwTzGAAjWBRLPeMpvsvNqENjMzvWwLJZlCpVAZvVwtJAeQ5NGAtNodHrfAZDG4wAAWRHGLygHShHyQMEkX3WNSJblh9BBeH8-iAA)*

Most of the Order’s properties are optional. It is fine. Let us look at one of the possible consumers of the above Order type.

```ts
export function createEmailMessage(order: Order) {
 if (order.expectedDelivery) {
  return `${order.name} will be delivered ${order.expectedDelivery}`
 }

 if (order.deliveredOn) {
  return `${order.name} has been delivered on ${order.deliveredOn}`
 }

 if (!order.expectedDelivery && !order.deliveredOn) {
  return `${order.name} is at ${order.at}`
 }
}
```

*[link to the TS playground](https://www.typescriptlang.org/play?#code/FAFwngDgpgBA8gJwCZQTAvDA3sGMDOIAhiAK74BcBICAlgHYDmuM9RAtlFYXUyyvgDGdCCFoB7egH5uNBszwkZMADLjBJCfRZQAHtEEgoSACJQANrQBuqMMpMko-C9dTG40qg6PAAvsGAAM1J6Qy0YYShHAFF2IlpzAFkofHwiRigACnFkVCpEFAQASmwWWkCYbNyEADo9AyNTFxsEMBKcPDwEKDIEehgAAwASLBzCmrZOXxgAdwTzGAAjWBRLFuMYEbHUOv0oQ2MzNdtfAZZ-Moqq8dXXbqQPdpYuntI+wa3qiY4oaYALIj4JZQKD9W7rJAwSSbUZfcFuB70U7nAJ4cqVACE21q9X2jSOdzAMAAZMSYFi4c0EY9Sp0YN1ev1hrDxpNfjBaECSDDsTUSMi8P5fEA)*

TypeScript helps us avoid accessing properties that could not exist, and we are safe.

What are we missing in the above example? **The whys**.

* Are we sure `status` is a generic `string`?

* Why is `description` optional?

* Why is `at` optional? Are we managing Orders that could not have an `at` Location? Such as services vs. goods?

* Why is `deliveredOn` optional? Their name suggests that the Order has been shipped. Do they depend on the Order’s status?

We cannot answer these kinds of questions without reading the product/domain documentation (if any) or without interrupting and boring our colleagues to understand when/why the mentioned properties exist or not. Even worse, **this uncertainty pours on the code**, causing a big mess in the codebase that gets worse and worse as it evolves.

Then, it turns out that the `status` is not a generic `string` but one of a finite set of states, such as `'ready' | 'inProgress' | 'complete'`. And **the optional properties** are not that optional, but they **are bound to the Order’s `status`**. It all makes sense, but how much time do I spend to get this info? What if there are 20ish entities whose types are like the `Order` one? It is what happened to me when I jumped on the project I mentioned earlier.

With the actual knowledge of the domain, let us refactor the `Order` type by leveraging TS Discriminated Unions.

```ts
type Order = {
  name: string
  description?: string
  at: Location
} & ({
  status: 'ready'
} | {
  status: 'inProgress'
  expectedDelivery: Date
} | {
  status: 'complete'
  expectedDelivery: Date
  deliveredOn: Date
})
```

*[link to the TS playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gTgEwnKBeKBvAUFXUB2AhgLYQBcUAzsHAJb4DmOeSlAxnWMLQPb4D8FanUbNchYBQAyPNhN74sAXygAyKAApseKsAkBXShQDkcCIQQhjyqAB9MY3QaNRj9AApweDM5UrWdCAAPSDZgCAQAEQgAG1oAN2QQCkiJCBt7bTxqZxM2HmIwGIhwgLxg0PCo2ISklLTHJDjEswQYfHrw5QBKLCwAM318MIUoBhKAUWJCWhiAWQg-QnGNHkRkCngkOG6HHUoAd1pgNgALTTXtgDoc4ENdrJ1cOUpoU3NLYzJHJ7wzO7g+CgAAMACQYS7IK5EUgqWiUKASKDgyFwK4SJTAvq-Z6EV6uDxeHyLfzfHFPf76QEglHrNEwiAqI4xGJQABG0CatVayIhdKuFQgYQi0WaSUx2JxLze+UKxVKZPJfxKVKBYL51wZKlOePZEAgQK5LQiiOAvNRVyNyAi7QlOiUyiAA)*

Now the future readers not only know when the properties exist but also why! And **it is a highly concise documentation** of the `Order` entity at the same time! I do not need to read anything but this type to get a precise idea of what the Order contains, and dealing with it gets a lot simpler. We moved from “oh, these properties are optional” to “oh, the order has three statuses, and when it is delivered, the delivery date is available”.

Do you remember the if-based `createEmailMessage` function? Now it’s

```ts
function createEmailMessage(order: Order) {
  switch(order.status) {
    case 'ready':
      return `${order.name} is at ${order.at}`

    case 'inProgress':
      return `${order.name} will be delivered ${order.expectedDelivery}`

    case 'complete':
      return `${order.name} has been delivered at ${order.deliveredOn}`
  }
}
```

*[link to the TS playground](https://www.typescriptlang.org/play?#code/KYDwDg9gTgLgBDAnmYcDyUAmwpwLxwDeAUHGXAHYCGAtsAFxwDOMUAlhQOannZMDG7MDDYQKAfkYt2XHmSoxGAGQj8FoisQC+cAGRwAFHPJkAPkWMmTLBQFcmjAORRgVTIkeXyWr2YtWA5hg7BzhHDgAFKAhOFyYmT0CrUBR+GGBMABFgABs2ADccREZMhWBfOB8k8xIk62CYeyd+CBowHOB0xLqyFOA0jOy8wqhiuFL0ipNsYZwMtAoSsoqq8gBKYk2AM1sKNI04Tk6AURoqNhyAWWB4qiODaGwoRgwntf9yJgB3Nhh+AAtDI8cAA6GyNJjvWqBNRMVDOVzuRz0KbkFyNKAUOAAAwAJIRgVAQdQ6Do2Ew4Ao4PjCSCFFpsZskrD4ZForEbgkUT0yOjbJicTSsKCScAdD8cjk4AAjVAzApzTDUgnCol9AZZXIK0YMpkwqhwsItNodLrcnl8gV4lVPYm0MVwf4GmXAYBY+UjDKU+BC20exULXUmHxaIA)*

The returned message did not change, but understanding the correct message for every Order status is.

And the readers are not the only ones that benefit from such readability! **TypeScript can better infer the returned type**. Look at what TS can infer from the first version of the `createEmailMessage` type and the second one.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vl6wtjijayfmsh490efi.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wmoeql4107i5mte2t5fz.png)

It means that consuming `createEmailMessage` turns from

```ts
const message = createEmailMessage(order)
if(message) {
  sendEmail(message)
}
```
to
```ts
const message = createEmailMessage(order)
sendEmail(message)
```

Multiply these benefits for the number of your entities and the size of your codebase… Do you get why I care so much about self-explanatory types? 😊

In the end, in the first `Order` type, we were missing the possibility of explaining the Order entity to the reader.



## 2. Straightforward JSX code

If I have not convinced you with the code of `createEmailMessage`, look at a standard JSX code.

```tsx
export function RenderOrder() {
 const [order, setOrder] = useState<Order | undefined>()

 useEffect(() => {
  fetch('https://api.yourdomain.com/latest-order')
   .then(response => response.json())
   .then(order => setOrder(order))
 }, [])

 const onSendEmailClick = useCallback(() => {
  if (!order) return

  const message = createEmailMessage(order)
  if (message) {
   sendEmail(message)
  }
 }, [order])

 if (!order) return null

 return (
  <div>
   <p>
    {order.name} ({order.status})
   </p>
   {order.description && <p>{order.description}</p>}

   {!order.deliveredOn && order.expectedDelivery && (
    <p>Expected delivery: {order.expectedDelivery}</p>
   )}
   {order.deliveredOn && <p>Delivered on: {order.deliveredOn}</p>}

   <button onClick={onSendEmailClick}>Send email</button>
  </div>
 )
} 
```

How many different DOM elements can result from the above JSX? Do you need less or more than five seconds to read it? Would you feel comfortable modifying it? And what about the React Hooks?

It is a simple example, and in a minute, you get an idea of whatever it does. But why should you waste this minute? And since it is just a simplified example, how long does it take to read a real-world component written like this?

The main problems are:

* React Hooks are great, **but they can make components’ code unreadable in no time**.

* **The JSX handles too many cases**. Building the states graph in your mind requires time.

My proposal is splitting the above component into many small components and custom hooks. Try reading the following code.

```tsx
type FetchStatus =
  | {
      status: 'loading'
    }
  | {
      status: 'success'
      order: Order
    }

export function RenderOrder() {
 const fetchStatus = useFetchOrder() // Return FetchStatus

 if (fetchStatus.status === 'loading') return <p>Loading...</p>

 const order = fetchStatus.order

 switch (order.status) {
  case 'ready':
   return <ReadyOrder order={order} />

  case 'inProgress':
   return <InProgressOrder order={order} />

  case 'complete':
   return <CompleteOrder order={order} />
 }
}

type Props = {
 order: Order
}

export function CompleteOrder(props: Props) {
 const { order } = props

 if (order.status !== 'complete') return null

 const { name, description, deliveredOn } = order

 return (
  <div>
   <OrderHeading name={name} description={description} />

   <p>Delivered on: {deliveredOn}</p>

   <SendEmailButton order={order} />
  </div>
 )
}
```

The crucial difference is **clarity**. You need more clicks to go down the rabbit hole to the exact component you are looking for, but clicks cost nothing when the code is straightforward. Instead, loading all the JSX states in our minds to guess what the component renders requires time and energy.

Please note: the `if (order.status !== ‘complete’) return null` in `CompleteOrder` is not optimal because we cannot add React hooks after the condition; hence we cannot have React hooks based on the properties of the completed Order. The problem, in this case, is that **we know that the component will only receive a completed order, but TypeScript does not know**. I don’t want to focus on this topic, but the quickest way to solve this problem is extracting a `CompletedOrder` type from the `Order` one:

```tsx
type CompletedOrder = Extract<Order, { status: 'complete' }>

type Props = {
 order: CompletedOrder
}

export function CompleteOrder(props: Props) {
 const { order } = props
 const { name, description, deliveredOn } = order

 return (
  <div>
   <OrderHeading name={name} description={description} />

   <p>Delivered on: {deliveredOn}</p>

   <SendEmailButton order={order} />
  </div>
 )
}
```

## 3. Explicit state machines

Everything is a State Machine. As programmers, we create State Machines in our minds before writing code. On the opposite, understanding the State Machines by reading the code is complex. State Machines’ details are hidden into the small components, hooks, functions, routes, atoms, stores, etc., that compose our app. It is a problem for the reader that cannot distinguish the “smart” parts of the app from the “dumb”/presentational ones:

* Who does read the initial data (a lot of times more components)?

* How does the app move from one state to another one?

* Who does change the state of the app?

* Who does react to every state change?

* In one question: how does the app work?

It depends on the app’s architecture, but usually, all the details of the State Machines that were in the author’s mind are not explicit. Even the apps rigorously based only on a global store could fail to explain how the app reacts to every state change (see the [“Redux is half of a pattern” article](https://dev.to/davidkpiano/redux-is-half-of-a-pattern-1-2-1hd7)).

Describing the whole app with a single State Machine is hard, but splitting the features in State Machines is crucial to ease the reader’s job. It is not important how we describe the State Machines, but having an entry point in the code where we explain most of the app/feature high-level flows is.

The first example comes from the app I cited at the beginning of the article. The side effects were spread all over the app, with many points setting the ([Valtio](https://github.com/pmndrs/valtio)-based) atoms, causing the app to re-render part or most of the UI tree. The things that can impact what the users see are:

1. If the users directly navigated to the app or the server redirected them, passing some data to the app

2. If the users connected the app to Shopify

3. If the users can view the app

4. If the users logged in

5. The intersection among the previous points

The React Hook that manages the state machine returns the following type (one status for every possible pages the users can see)

```ts
type AppStatus =
  // initial value
  | { status: 'idle' }
  | { status: 'showLogin' }
  | { status: 'showImport' }
  | { status: 'showConnect' }
  | { status: 'showNonAdminError' }
  | { status: 'loadingUserSession' }
  | { status: 'showGenericInstructions' }
  | { status: 'showAllOrdersSelectedError' }
  | { status: 'showSelectOrdersInstructions' }
```

And the State Machine is a big `useEffect` composed by two nested `switch` statements with a code like this

```ts
switch (currentPage) {
  case 'connect':
    switch (howUserNavigated('connect')) {
      // ------------------------------------------------------------------
      // SCENARIO: the server redirected the user to the connect page
      // ------------------------------------------------------------------
      case 'sentFromServer':
        switch (connectStatus.status) {
          case 'notRequestedYet':
          case 'requesting':
          case 'failed':
            // when the connect succeeds, this effect is re-triggered
            setStatus({ status: 'showConnect' })
            break

          case 'succeeded':
            setStatus({ status: 'showSelectOrdersInstructions' })
            break
        }
        break

      // ------------------------------------------------------------------
      // SCENARIO: the user navigated directly to the connect page
      // ------------------------------------------------------------------
      case 'directNavigation':
        redirectTo('home') // as a result, this effect is re-triggered
        break
    }
    break
```

You can argue that two nested `switch` aren’t great, but I see value in a single file where the reader can understand everything about the high-level, domain data-driven pages management. You can find the complete code of the Hook in this [Gist](https://gist.github.com/NoriSte/f3d9ddc90d2195fc0884105a8a90f899).

The next step is describing the State Machine through something made on purpose like XState. Here is an example from my recent [“How I strive for XState machine, types, and tests readability” article](https://dev.to/noriste/how-i-strive-for-xstate-machine-types-and-tests-readability-19f4).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tk4lnjoiz8ybaiuj0otv.png)
 

There is nothing more explicit than a centralized and viewable State Machine. More: Xstate allows you to create a working and UI-free prototype in no time.

Thanks to explicit State Machines, the reader can see how your app/feature internally works.



## 4. Cypress integration tests

We spoke about code and never about the app from a user perspective. But there are some crucial topics to show to effectively onboard new developers:

* Describing what the app does from a User perspective: I want to show the UI, the code doesn’t matter.

* Describe the order of the user actions and the interactions with the back-end.

* Working against a controlled back-end

Here Cypress comes in handy with its ability to stub the back-end and its expressive APIs. Here is an video and an example of the code behind the tests

{% youtube ae4_KKzDeCw %}

```js
it('When the server sends the users to the connect page, should show the "connect" page', () => {
  visitAndResetSessionStorage('/connect?nonce=12345678')

  // --------------------------------------------------------------------------------------
  cy.log('--- Should bring to the connect page ---')
  cy.findByRole('button', { name: 'Connect' }).should('be.visible').click()

  // --------------------------------------------------------------------------------------
  cy.log('--- Should show an error if connect fails ---')
  cy.findByLabelText('Username').type('smagni', { delay: 0 })
  cy.findByLabelText('Password').type('smagni', { delay: 0 })
  cy.findByRole('button', { name: 'Connect' }).click()
  cy.findByText('Something went wrong (Error -1)').should('be.visible')

  // --------------------------------------------------------------------------------------
  cy.log('--- Should show the import orders instructions if connect succeeds ---')
  cy.intercept('POST', '**/shopify/connect', { fixture: 'importOrders/happyPath/connect' }).as(
    'connect-request',
  )
  cy.findByRole('button', { name: 'Connect' }).click()

  // --------------------------------------------------------------------------------------
  cy.log('--- Should pass the user data to the server ---')
  cy.wait('@connect-request').its('request.body').should('deep.equal', {
    nonce: '12345678',
    password: 'smagni',
    username: 'smagni',
  })

  // --------------------------------------------------------------------------------------
  cy.log('--- Should show the "select orders" instructions if connect succeeds ---')
  cy.findByRole('button', { name: 'Select orders' }).should('be.visible')
})
```

By watching Cypress controlling the app and the app reacting to the interactions, it is straightforward to understand what the users can do and what they are not expected to do. The tests tell when the AJAX requests happen, thanks to the controlled server.

## Conclusions

Now, I expect the next developers that must introduce a feature to:

2. **Watch the feature-related tests** to comprehend what the app does.

3. **Read the feature-related tests** to comprehend the data passed through the URL.

4. What **data the app sends to the server** and when.

5. What **data the app receives** from the server.

6. Read and understand the **state machine** behind the mentioned flows.

7. Quickly move through the **React components**.

8. Find all the **domain-related knowledge in the types**.

9. Blaming me because I don’t write code like them, but at least not spending roughly one week gathering all the domain knowledge I then made explicit through the code, the State Machine, and the tests 😊.

There are other important things to know for a long-living project, but the four I described are essential, in my opinion 😊.