Level up your Typescript game, functionally - Part 1
Tags: typescript, functional, programming, webdev
Published: 2023-11-25 01:10:18

Welcome to the first post in the series "Level up your Typescript game, functionally".

Typescript is awesome. It is basically Javascript with superpowers. I can't imagine writing Javascript code today without types. 

### Statistics

Let's take a look at the state of programming languages. When we compare 2020 to 2023, we can see that there is rise in the popularity of **Typescript**, **Python** and **GO**.

Language | % of coders | % of coders | % change
--- | --- | --- | --- |
 | **2020** | **2023**
Javascript | `67.70` | `63.61` | 🔻 `4.09`
**Typescript** | `25.40` | `38.87` | 🔺 **`13.47`**
HTML/CSS | `63.10` | `52.97` | 🔻 `10.3`
**Python** | `44.10` | `49.28` | 🔺 **`5.18`**
SQL | `54.70` | `48.66` | 🔻 `6.04`
Java | `40.20` | `30.55` | 🔻 `9.65`
Shell (Bash etc.) | `33.10` | `32.37` | 🔻 `0.73`
C# | `31.40` | `27.62` | 🔻 `3.78`
PHP | `26.20` | `18.58` | 🔻 `7.62`
C++ | `23.90` | `22.42` | 🔻 `1.48`
**Go** | `8.80` | `13.24` | 🔺 **`4.44`**

Source: Stack Overflow Survey [2020](https://survey.stackoverflow.co/2020/#most-popular-technologies) & [2023](https://survey.stackoverflow.co/2023/#technology)

Let's take a look at the rising popularity of Typescript when compared to Javascript in the web ecosystem.

![js-vs-ts-balance-img](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y4jflemk9edun6unx9sw.png)

Source: [2022 State of JS](https://2022.stateofjs.com/en-US/usage/#js_ts_balance)

We can certainly infer that Typescript adoption has grown and is steadily on the rise.

By the way, here's a fun [documentary video](https://www.youtube.com/watch?v=U6s2pdxebSo) about the history of typescript. It has a lot of interesting tidbits about timing and decisions that were made in the early days.  

### State of the union

Today there's tonnes of information and [communities](https://www.typescriptlang.org/community) around typescript. 

Note: If you are new to typescript, I highly recommend heading over to [TotalTypescript](https://www.totaltypescript.com/) and checking out the professional tutorials by the experienced [Matt Pocock](https://www.mattpocock.com/)

Here's some sample type safe JavaScript code using Typescript. 

```typescript
type User = {
  id: number,
  name: string
}

const getUser = (): User => {
  return {
    id: 1,
    name: 'John'
  }
}
```

The above code ensures that at compile time we have checked our types and are operating on safe code. 

What about asynchronous code involving I/O? 

Well...this is what it looked like before 2017.

```typescript
const aPromise = (): Promise<Data> => {
  // Some API call / IO 
  return Promise.resolve(data)
}

const main = () => {
  aPromise().then(data => {
      // Do something with result
  })
}

main().then(...)
```

It doesn't look great but it got better after the addition of `async`/`await` which was introduced as part of the ECMAScript 2017 standard.

```typescript
const aPromise = async () => {
  // Some API call / IO 
  return Promise.resolve(data)
}

const main = async () => {
  const result = await aPromise()
  // Do something with result
}
```

But what about errors? Well...we can use `try/catch`

```typescript
const main = async () => {
  try {
     const result = await aPromise()
    // Do something with result
  } catch (err) {
    // Do something with error
  } finally { // optional block
    // No matter what happens run this code
  }
}
```

This definitely works and does a much better job handling errors and unexpected things with I/O. But to be honest, it's not the cleanest code to look at.

Also what happens when you have to write a lot of I/O code that can potentially fail? 

You get into try/catch hellscape of course.

```typescript

// This promise will succeed
const promise1 = async () => { return Promise.resolve(1) }

// This promise will return an error
const promise2 = async () => { return Promise.reject(new Error('boom') }

// This promise could return an error or succeed
const promise3 = async () => { 
  const promiseResults = [
    Promise.resolve(1), 
    Promise.reject(new Error('boom')
  ]

 // Randomly pick the success or error promise
 return promiseResults[
   Math.floor(
     Math.random() * promiseResults.length
   )
 ]
}

const getRequiredData = async () => Promise.all(
  promise1, 
  promise2
)

const getResult = async (data) => promise3(data)

// Catch and throw
const app1 = async () => {
   try {
     const data = await getRequiredData()
     const result = await getResult(data)
     return result
   } catch(err) {
      // Note that we can't tell what promise failed from the try block
      console.error(`Something failed in the promise block`, err)
      throw err
   }
}

// OR don't catch anything and let it bubble up. 
// Looks nicer but no utility
const app2 = async () => {
  const result = await getResult(await getRequiredData())
  return result
}

```

Notice the difference between `app1` and `app2` above. 

In the `app1` case, we catch everything in the chain and can handle it but we have no information on what caused the error or what the source was, we simply know that something failed in the higher order operation. Furthermore, writing `try/catch` blocks for every async call in our entire app flow is going to get pretty messy, pretty fast. 

In the case of `app2`, we don't do any error handling and let everything bubble up to the caller. If this was the root call, our app would have crashed with no handling; and even worse, if we didn't have a process monitor to reboot our app when it fails, we're out of luck unless we manually intervene.

Both of the above cases are not ideal. We can do better. **We must do better**.

### Why are exceptions bad?

Here's a good breakdown from Dan Imhoff's [post](https://imhoff.blog/posts/using-results-in-typescript)

> **Exceptions are not type-safe**. TypeScript cannot model the behavior of exceptions. Checked exceptions (like in Java) will likely never be added to TypeScript. Not all JavaScript exceptions are Error objects (you can throw any value). JavaScript exceptions have always been and will always be type-unsafe.

> **Exceptions are often difficult to understand**. It is unclear whether or not a function throws exceptions unless it is explicitly documented. Even source code inspection makes it difficult to know if exceptions might be thrown because of how exceptions propagate. On the other hand, if a Result-returning function is inspected, the error cases become immediately clear because errors must be returned.

> **Exceptions (may) have performance concerns**. This isn’t hard science and likely completely negligible, but JavaScript engines in the past have struggled to optimize functions that use exceptions because pathways become unpredictable when the call stack can be interrupted at any time from anywhere.

So in summary, exceptions are pretty bad and unreliable to code against. 

Now let's take a look at a solution to model our operations without using exceptions by creating custom functional types in Typescript.

### The `ResultTuple` type

First let's represent our success and error using unit types

```typescript
// An operation result can be anything
type OpResult<T> = T

// An operation error can be any custom error type or the Error type itself (default)
type OpError<E = Error> = E
```

Next, let's create a common type to represent the result and/or error of an operation.

```typescript
// A unified type to represent the operation 
// result with both the success and error types.
// Either one or both can be defined
type ResultTuple<T, E = Error> = [ OpResult<T>?, OpError<E>? ]
```

Let's say we have a function that takes in a list of numbers and sums them. This is what the signature would look like

```typescript
const add = (nums: number[]): number => {
  return nums.reduce((acc, c) => acc + c, 0)
}
```

To be more declarative, using the newly created `ResultTuple` type we can now represent the same function as follows.

```typescript
// Return type with explicit result and implicit Error type
const add = (nums: number[]): ResultTuple<number> => {
  // first argument is the success type
  // second argument is undefined; same as [result, undefined]
  return [
    nums.reduce((acc, c) => acc + c, 0)
  ]
}
// OR Return type with explicit Error type
const add = (nums: number[]): ResultTuple<number, Error> => {
  return [
    nums.reduce((acc, c) => acc + c, 0)
  ]
}
```

We have created an explicit definition for the function that has both a success type and the error type. So callers can infer more from the result via `ResultTuple`.

```typescript
const app = () => {
  // We can easily infer error or result from the operation
   const [result, error] = add([1,2,3])

   // If error is defined, do something with it
   if (error) { throw error }

   // If result is defined, do something with it
   if (result) { return result }
}
```

With this approach to the `add` function, we can de-structure both the result and error from the function call and act appropriately. This makes so much more sense because it's declarative and explicit and you know what you're getting when you call the `add` function. 

Now, let's look at an error handling case

```typescript
const divide = (dividend: number, divisor: number): number => {
  if (divisor === 0) throw new Error('Cannot divide by zero')
  return dividend/divisor 
}

// Rewrite the same function using our ResultTuple type

const divide = (
  dividend: number, 
  divisor: number
): ResultTuple<number> => {
  if (divisor === 0) {
    return [
      undefined,
      new Error('cannot divide by zero')
    ]
  }
  return [dividend/divisor]
}

const app = () => {
  const [result, error] = divide(1, 0)
  if (error) { throw error }
  if (result) { return result }
}
```

This works really well, doesn't it?

Now let's take a look at an asynchronous example. As we've seen before; this is how we would do it if we only used `try/catch` to handle our exceptions everywhere.

```typescript
// A sample result data type
type Data = { id: string, value: string }

const getDataFromDB = 
  async (ctx: Context): Promise<Data> => {
    try {
      const result = await ctx.db.primary.getOne()
      return result
    } catch (err) {
      console.error('Error getting data from DB', err)
      throw err
    }
  }

// We have to catch here too and throw
const app = async () => {
  try {
   const data = await getDataFromDB(ctx)
  } catch (err) {
    throw err
  } 
}
```

Now let's add in the new paradigm of `ResultTuple` and this is what we would get

```typescript
const getDataFromDB = 
  async (ctx: Context): Promise<ResultTuple<Data>> => {
    try {
      const result = await ctx.db.primary.getOne()
      return [result]
    } catch (err) {
      console.error('Error getting data from DB', err)
      return [undefined, err]
    }
  }

// The wrapper call becomes really simple
const app = async () => {
   const [result, error] = await getDataFromDB(ctx)
   if (error) { throw error }
   if (result) { return result } 
}
```

Using this pattern, our operations become more explicit and are super easy to reason about. 

Note that we only used `try/catch` around the fn `ctx.db.primary.getOne()` to get data from the DB assuming we don't control this operation and it's behavior. We could also easily create a wrapper for the DB so we can catch errors internally and expose the the `ResultTuple` to callers. 

You can also get really loose or strict with the types. For example, what if you wanted to define your own error type, you can do it this way.

```typescript
// Custom error class that extends the JS Error interface
class AppError implements Error {
   name: string
   message: string
   constructor(message: string) {
    this.name = 'AppError',
    this.message = message
   }
}

// We create a new ResultTuple which overloads our Error type
// to be our base AppError instead of the default Error interface
type CustomResult<T, E = AppError> = ResultTuple<T, E>
```

Then you can use `CustomResult<T, AppError> or CustomResult<T>` everywhere instead of `ResultTuple<T, E>`.

```typescript
const mySuccessFn = (): CustomResult<string> => { 
  return ["hello world"]
}
// OR
const myErrorFn = (): CustomResult<string> => {
  return [undefined, new AppError('boom')]
}
```

Using this pattern you can get more advanced with your error handling too

```typescript
class AppError implements Error { ... } // see above
class DBError extends AppError { ... }
class OtherError extends AppError { ... }

// We can use the same custom result type
type CustomResult<T, E = AppError> = ResultTuple<T, E>

// Example use case where op fails when fetching data from DB
// Note that CustomResult already has AppError as the default
// Error type which means any class that inherits AppError 
// such as DBError can be safely returned
const getFromDB = (): Promise<CustomResult<Data>> => {
  try {
    ...
  } catch (err) {
    return [undefined, new DBError(...)] // works!
  }
}
```

The original definition of `ResultTuple` was designed in a way where either argument was optional. In order to tighten the type and usage, we can re-write it so that at most one argument should be specified that way each operation can only return a result or error but not both. 

```typescript
// Original definition where either argument can be present or undefined
type ResultTuple<T, E = Error> = [ OpResult<T>?, OpError<E>? ]

// New definition where only one argument can be defined
type ResultTuple<T, E = Error> = 
  [ OpResult<T>, undefined ] | [ undefined, OpError<E> ]

```

The new definition ensures that either the result or error is defined but not both at the same time. This is even better because this makes all operations that return this type choose one or the other and return data correctly. This is probably the safer pattern but it depends on your use case. What if you have operations that return partial results and errors? In that case, you are better off using the original definition so you can return both partial results and errors so callers can do as they please with that information. For example `return [partialResults, partialErrors]`

So we went through a lot of stuff in this article but in summary we saw how we went from traditional typescript programming to a pattern of functionally representing the results of operations using the `ResultTuple` type which is more declarative and scales based on practical use cases and avoids the Exception handling nightmare.

What if we wanted to make this even better? Typescript is so powerful and packed with so many features that we could use it to our advantage to boost our productivity and be declarative for complex use cases and flows.

In the next post, we will take a deeper look at functional programming and how we can apply it to Typescript to truly level up your code!

Congrats on making it to the end of this post. You have leveled up 🍄

If you found this post helpful, please upvote/react to it and share your thoughts and feedback in the comments.

Onwards and upwards 🚀