Dependency Injection without classes
Published: 2024-01-06 10:17:10


**Dependency Injection (DI)** is a pattern that enhances the maintainability, scalability, and testability of your code. In TypeScript, we can leverage higher-order functions to implement Dependency Injection seamlessly, promoting a more modular and flexible codebase.

While it may sound like a complex pattern, many tools out there make it look very complicated. However, the reality is that DI is simply passing parameters, mostly to the constructor. As Rúnar Bjarnason casually once said, dependency injection is "really just a pretentious way to say 'taking an argument.'"

To provide a short and easy example, let's imagine we are working on a cab (taxi) Uber-like application. We've received a request to implement a rides pricing feature. For the sake of this article, the only rule is that the price of a ride must be $2 per kilometer for a given route.

## Example

- The distance from my house address to the airport is 20 KM for the available itinerary.
- The price returned by the application should be 20 * 2 = $40.

We'll follow the domain model:

```ts
type Address = {
  street: string;
  city: string;
}

type Route = {
  origin: Address;
  destination: Address;
};

type Itinerary = {
  route: Route;
  distance: number;
}

type PricedRide = {
  itinerary: Itinerary;
  price: number;
}
```

However, the implementation of the ride pricing feature needs to know the itinerary to calculate the price based on the distance. The route itinerary can be fetched using a third-party service like Google Maps, Google Addresses, or others. Let's assume we haven't made a decision on which service to use.

## Implementation using class-based Dependency Injection:

Since the itinerary 3rd-party service provider is not known yet, we can abstract it behind an interface and inject it into the class that calculates the price as a constructor parameter, like the following:

```ts
interface ItineraryService {
  getItinerary(route: Route): Promise<Itinerary>
}

class RideService {
  constructor(private itineraryService: ItineraryService) {}

  async priceRide(route: Route): Promise<PricedRide> {
    const itinerary = await this.itineraryService.getItinerary(route)
    const price  = itinerary.distance * 20;
    return {
      itinerary,
      price
    }
  }
}
```

Let's break down the code above

- `ItineraryService` is an interface that exposes the `getItinerary` method signature. The implementation details of that interface can be done once a decision is made on what itinerary service provider to use, or even use a fake implementation.
- `RideService` class has a constructor that takes an ItineraryService implementation instance. It doesn't matter what the concrete implementation to use is since it respects the interface contract.
- `RideService` exposes the `priceRide` method that takes a Route object and returns a promise of a `PricedRide` object after getting an itinerary for the passed route.
 
I kept the code short by removing some details like error handling.

## Towards function-based DI

Now let's try to get rid of that verbose class-based version and replace it with another version that uses just functions. Since DI is basically passing parameters and functions are first-class citizens in JavaScript and TypeScript, we can just use a function to calculate the price and pass the itinerary fetching dependency as a function as well. The class-based code can be refactored to:

```ts
const priceRide = async (route: Route, getItinerary: Function): Promise<PricedRide> => {
  const itinerary = await getItinerary(route)
  const price  = itinerary.distance * 20
  return {
    itinerary,
    price
  }
}
```

The code is now shorter and simpler, isn't it?

We have a small issue in the function implementation above: `getItinerary` function type accepts any function. Let's fix this by adding the following type to our domain model:
```ts
type GetItinerary = (route: Route) => Promise<Itinerary>
```
## Dependencies First, Data Last
If we want to mimic the class-based approach, we can just **FLIP** the order of the `priceRide` function arguments like the following:
```ts
const priceRide = async (getItinerary: GetItinerary, route: Route) => {
  const itinerary = await getItinerary(route)
  const price  = itinerary.distance * 20
  return {
    itinerary,
    price
  }
}
```

Flipping the arguments order as above is more of a functional style, where the input data are always set as the last function argument. This will allow us to partially apply `priceRide`.

## Partial application

It is a functional programming concept that can be applied to create a new function by just passing some arguments to a function. For example, we can use `lodash.partial` to get a new function from our `priceRide` like the following:
```ts
const partialPriceRide = _.partial(priceRide, getItinerary) // create a function with a dependency
async partialPriceRide(route) // -> ride price
```
Where `getItinerary` and route are concrete implementations.

This is exactly the steps to make to use the class-based version:

```ts
const rideService = new RideService(itineraryService) // create an object with a dependency
async rideService.priceRide(route) // -> ride price
```

Great! Now, since partial application is a function that returns a function, instead of using `lodash` to create our `priceRide` function, we can define the function like the following:
```ts
const priceRide = (getItinerary: GetItinerary): PriceRide => async (route: Route): Promise<PricedOrder> => {
  const itinerary = await getItinerary(route)
  const price  = itinerary.distance * 20
  return {
    itinerary,
    price
  }
}
```
We need to add `PriceRide` function definition to the domain model:
```ts
type PriceRide = (route: Route) => Promise<PricedRide>
```

- The function now returns a function instead of a promise of `PricedRide` by taking all dependencies as parameters.
- The returned function returns a promise of `PricedRide` and uses the dependency to fetch an itinerary.
- Since `priceRide` creates a closure, all dependencies are accessible for the returned functions but private for the outside world (like in the class-based version).

## Runtime configuration checking

We can take advantage of the `priceRide` function and add a runtime check that verifies that the passed `GetItinerary` parameter is really a function:

```ts
const priceRide = (getItinerary: GetItinerary): PriceRide => {
  if (typeof getItinerary !== "function") throw new TypeError("Get Itinerary is not a function")
  return async (route: Route): Promise<PricedRide> => {
    const itinerary = await getItinerary(route)
    const price  = itinerary.distance * 20
    return {
      itinerary,
      price
    }
  }
}
```

The major downside of this approach is that the function creates a closure for every call, which means a new object is created every time we call the function, however this can avoided by calling the function once at the composition root level which will be a topic for another article.