Webpack 5 introduced support for module federation. This is the ability to load modules at runtime instead of compiling them into your JavaScript application. However, while Webpack solves the runtime composition need, it leaves the compile-time needs of tools like TypeScript unsolved. We’re going to need more tooling if we want to get good type information in an application that uses federated modules.
What is module federation?
Fundamentally, Webpack’s module federation enables runtime composition of an application from modules instead of forcing all dependencies to be resolved and composited at compile time. A module included via module federation won’t be included in the bundled JavaScript for the application. Instead, a small Webpack runtime embedded in the application will load the module at run time, usually by downloading the module’s code from a configured URL.
Module federation allows module authors to publish updates that automatically get included in deployed applications. They do this without requiring a rebuild and redeploy cycle for each application using the module, assuming there are no breaking changes in the update.
We’re interested in using Webpack 5’s module federation as part of a microfrontend architecture. That will give our teams more independence as we collaborate on a suite of applications and shared components. Microfrontends aren’t the only thing you can use module federation for, but it’s certainly where a majority of the interest seems to be right now.
One note of caution: Module federation in its current state is unsuitable for loading modules from internet sources. Runtime composition requires more trust in the module publisher compared to compile time composition. That’s because a federated module could be changed at any time and immediately affect application users. Module federation is a good fit for internal modules and microfrontends, assuming high-trust relationships between application and module teams.
Why are TypeScript types a problem?
It’s challenging to build and acquire good TypeScript types for a federated module. That’s primarily because Webpack module federation only loads resources from the federated module at runtime, but TypeScript needs the type information at compile time. There’s no built-in way to publish and acquire the compile-time types.
Even if we could easily publish and load the type declaration files produced by the TypeScript compilation process easily, module federation changes module names and paths.
- When publishing a container (a collection of modules) for use via module federation, you must specify a name for the container and each module it exposes. These names don’t have to match the names/paths of the TypeScript code they reference. So, types from the compilation process aren’t guaranteed to match what gets exposed via the federated module.
- When using a module via federation, the consuming application can choose a different name for the container. So even if naming is consistent for the internal TypeScript code and published modules, the user of the module could name the container differently.
What are our options?
Empty Module Declaration in Host Application
Most examples of module federation using Typescript add an empty module declaration to make the TypeScript compiler happy. However, they don’t add actual, helpful types to the consuming application. This can get the complication process working but leaves a gap that makes developers who like TypeScript types (like me) unhappy.
It might be totally fine to use this approach if the interface exposed by the module is simple and small and infrequently changed. Otherwise, I’d consider beefing up the type declarations to provide more of the support we expect from TypeScript.
Hand-written Module Declaration in Host Application
If the empty module declaration isn’t enough, we could copy or hand-write the type declarations we care about for the interface of the federated module. The declarations need to be present in each application that uses the module and you may need to customize them. That means you must do maintenance and there’s a risk of things getting out of date, but it could work.
Reference Types Across a Monorepo
If the module and applications that use it live together in a monorepo, you have more options. Those options can reduce manual maintenance using relative filesystem paths to load generated type declaration files. The pixability/federated-types module could help wrangle the generated .d.ts
files. The documentation there shows how one might configure the monorepo to share the types.
We decided not to go this route because we have applications that will use our module from outside our monorepo.
NPM Package with Types
Publishing an NPM types package sounds like an easy solution. However, the malleability of container and module names poses a restriction. The publisher and consumer of the federated module must use a naming scheme matching the internal TypeScript code. See this example on GitHub issues for more details on one convention that seems to work.
Because of that shortcoming, we opted not to pursue this route either.
Types Deployed Alongside JavaScript Distributable
During my research, I came across a couple of small tools that would allow us to modify TypeScript type declarations to match the interface of our federated module. They could also deploy the declaration files alongside the JavaScript code and load the type declarations from a URL while applying transforms to match the consuming application’s configuration. This was the closest thing I saw to a complete solution. It works for monorepos, split repos, and applications that rename federated modules they include. I also like the symmetry of deploying the types in the same manner as the JavaScript code.
Key tools that enable this approach include:
- dts-loader – a Webpack plugin that collects .d.ts files and emits type declarations that match the federated module’s configuration
- webpack-remote-types-plugin – a Webpack plugin that manages the downloading of a remote tarball containing types
The steps to set up both tools are well documented on their respective GitHub pages, so I won’t repeat the detail here. But, in brief form:
- Configure
dts-loader
with the same information used to configure theModuleFederationPlugin
in Webpack, and it will emit type declaration files for each exposed module. - Add a step to the module’s publish workflow that bundles the resulting
.d.ts
files into a tar file and copies it alongside the JavaScript bundle - Configure the
webpack-remote-types-plugin
to load the types from the resulting URL
That should close the loop and allow a consuming application to use types exported by a federated module. It’s a good idea to include the outputDir
from the webpack-remote-types-plugin
config in the .gitignore
for the project.
Where can I read more?
If you’re interested in digging deeper, here are a few of the resources I found helpful during my research