DEV Community

David Rickard
David Rickard

Posted on

Dependency Injection with Webpack and TypeScript

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:

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();
Enter fullscreen mode Exit fullscreen mode

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:

export const platform: PlatformApi = {
    getIdToken: async (): Promise<string> => "",
    getDeviceName: (): string => "",
}
Enter fullscreen mode Exit fullscreen mode

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:

export const platform: PlatformApi = {
    getIdToken: async (): Promise<string> => {
        // Android implementation
    },
    getDeviceName: (): string => {
        // Android implementation
    },
}
Enter fullscreen mode Exit fullscreen mode

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

webpack.config.js


const webpack = require('webpack');

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

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()

Latest comments (0)