Dependency Injection with Webpack and TypeScript
Tags: typescript, webpack
Published: 2022-04-26 06:29:23

The web is a great way to render an app's interface on any platform, but you often need to do the same action differently based on what platform you're on. WebView callbacks are message-based on iOS, but are direct method calls on Android.

Doing checks all over your code is ugly, so you might decide to export a different object depending on the platform:

``` typescript
interface PlatformApi {
    getIdToken: () => Promise<string>;
    getDeviceName: () => string;
}

function getPlatformImplementation(): PlatformApi {
    if (onAndroid) {
        return {
            getIdToken: async () => {
                // Android implementation
            },
            getDeviceName: () => {
                // Android implementation
            }
        }
    }

    if (onIOS) {
        return {
            getIdToken: async () => {
                // iOS implementation
            },
            getDeviceName: () => {
                // iOS implementation
            }
        }
    }

    // ...
}

export const platform: PlatformApi = getPlatformImplementation();
```

This is better and allows anyone to make a platform call by calling `platform.getDeviceName()`.

But it's still not ideal. You're bundling up code for all platforms, leaving some useless JS weight that Webpack doesn't know how to tree-shake away.

## Module replacement

Our savior here is Webpack's `NormalModuleReplacementPlugin`. You can make a "stub" module that everything imports and calls:

**MyAppPlatform.ts:**
```typescript
export const platform: PlatformApi = {
    getIdToken: async (): Promise<string> => "",
    getDeviceName: (): string => "",
}
```

But then in different webpack configs, do a different module replacement for each platform. For example, with Android, first make a module that exports an object that implements the shared `PlatformApi` interface with Android-specific code:

**AndroidPlatform.ts:**
```typescript
export const platform: PlatformApi = {
    getIdToken: async (): Promise<string> => {
        // Android implementation
    },
    getDeviceName: (): string => {
        // Android implementation
    },
}
```

Then tell Webpack to swap out the stub module with your platform module:

**webpack.config.js**
```javascript

const webpack = require('webpack');

// Inside the webpack config object:
plugins: [
    new webpack.NormalModuleReplacementPlugin(
        /\/MyAppPlatform$/,
        (resource) => {
            resource.request = resource.request.replace(
                /MyAppPlatform$/,
                "AndroidPlatform");
        }),
],
```

You have to do the fancy replacement function because people might be referencing your module from anywhere. The regex is looking for any import that ends in `/MyAppPlatform`, then replacing that last section with the `AndroidPlatform` module that's right beside it. It's a good idea to add your app name to your stub modules, so you don't accidentally replace some module deep inside a library you're using. This happened to me!

Anyway, with the Webpack module replacement approach to dependency injection, you get:

* Type safety because all platforms and callers have to conform to the same interface
* Efficient bundling because every platform is built with only its own code
* Easy use of the module with calls like `platform.getDeviceName()`