Lately, I’ve been trying to upgrade the code base for a personal project with some legacy code in it. Specifically, it’s a web application built with Typescript that:

Contains a manually injected <script> tag in the index.html file for each JavaScript file loaded (i.e. JavaScript files are not bundled using Browserify or Webpack) Doesn’t leverage CommonJS modules (instead, there are globally namespaced variables)

The purpose of this post is to discuss some of the lessons I learned when transforming the code base from using legacy features to using some better Typescript features. Specifically, I’ll highlight the difference between Typescript internal and external modules, as well as explain the benefit of using a module bundler like Webpack.

Starting Scenario

For the purpose of this case study, I’ll create a starting point that mimics the project I was working on but simplified for our purpose. I’ve created a Github repo for it — you can see it here (be sure to check out the “no-bundler” branch).

I’ll go ahead and re-produce the 4 main files below as well:

src/module1.ts

src/module2.ts

src/app.ts

index.html

If you want to see the results, be sure to clone down the repo and then run the following 2 commands on the command line:

npm install npm run build

The 1st command will install all of the NPM dependencies, and the 2nd command will transpile the Typescript files to JavaScript. If you open your browser and look at your console, you should see 2 console logs.

Let’s talk a little bit about the way these files are currently set up.

The “module1.ts” file defines a single function “sayHello” — nothing out of the ordinary here. What’s important to note though is that we are transpiling this file to its JavaScript equivalent and then loading it into the index.html as part of the <script> tag. This implies that the “sayHello” function will be available in the global namespace. The “module2.ts” file also defines a single function “add” but notice that its wrapped within a “namespace” block and “exported”. In Typescript, this produces an Internal Module, which is really just a Plain Old JavaScript Object that’s been assigned a name (in this case, “InternalModule” is the name). For those who want to confirm this for themselves, take a look at the “module2.js” file within the repo’s “build” directory. Similar to “module1.ts”, “module2.ts” is also transpiled to its JavaScript equivalent and loaded into the index.html file as part of a <script> tag. However this time, the “InternalModule” object is available in the global namespace, and this object will have the “add” property on it. The “app.ts” file simply calls the 2 functions provided by the previous 2 files. It can do this because all <script> tags load JavaScript into the same global namespace — this is why “app.ts” has access to those functions. Something subtle to notice that has a HUGE impact is the ordering in which the <script> tags are loaded in the index.html file. Because all the functions are loaded into the same global namespace, and because app.ts relies on both functions being available, app.ts must be loaded last (if not, an error will be thrown in the console). In our simple example, it doesn’t matter whether module1.ts or module2.ts is loaded first because neither of these files have other dependencies. This is an important point though because things get a lot more complicated in a realistic scenario where module1.ts and module2.ts also depend on other files. And its these realistic yet complicated scenarios that lead to the need for a module system like CommonJS and a module bundler like Webpack.

For my simplistic starting scenario above, you’re right that the code can probably be left this way and still be ok. But you can already see that this code isn’t ideal because:

We’re polluting the global namespace We need to start worrying about the order in which files are loaded into the <script> tags in our index.html. The files have “dependencies” on each other, and as these dependencies start to grow with application size, we’ll really be in trouble.

So let’s fix things up!

Ideal Scenario

If you’re itching to see how the improved version looks like, check out the same previous Github repo but look at the “master” branch. Remember to clone down the repo, checkout the master branch, and then run the same 2 commands in your command line:

npm install npm run build

You’ll notice that the “npm run build” command does an extra step in this scenario, specifically, we’re now leveraging Webpack to trace through the different module dependencies and then bundling the different JavaScript files into 1 final file. We’ll discuss the details more in a bit.

But first, let’s look at the same 4 files as before:

src/module1.ts

src/module2.ts

src/app.ts

index.html

Let’s do a step-by-step comparison for these files across the 2 versions:

The “module1.ts” file is identical to the previous version EXCEPT that it includes the keyword “export” before the “sayHello” function. This is Typescript’s support for modules :) We are now treating this file as a module, and in order to use this module within other files, we’ll need to import this module. This is going to sound anti-climatic but Typescript’s concept of External Modules is exactly this — the ability to create modules that can export different functionality and then import those modules into other modules. The “modules2.ts” file is also nearly identical to the previous version EXCEPT we’re exporting the Typescript namespace object (i.e. Typescript Internal Module). Remember that Typescript internal modules are literally plain old JavaScript objects, so in this case, what we’re actually doing is just using a slightly different syntax to export an object (whereas “module1.ts” was exporting a function) that has the “add” function on it. The major consequence of the changes to module1.ts and module2.ts is that the “sayHello” function and the “add” function are NOT available in the global namespace. This is absolutely fantastic news because we no longer have to worry about naming collisions and polluting the global namespace! As I mentioned in point 1 above, if we want to use the “sayHello” and “add” functions , we’ll now need to import each of them into the file that uses it before we have access to them. And this is exactly what we’re doing in “app.ts” now. Compared to the previous version of “app.ts”, we now have 2 import statements at the top of the file which exposes the “sayHello” function on an object called “SomeModule” and the “add” function on an object called “AnotherModule”. If you try calling “sayHello” or “add” without preceding them with their respective module name, you’ll see an error! The last thing I want to point out is a consequence of using a module bundler like Webpack. If we look at the “index.html” file, we now see that there’s only 1 <script> tag and its for a “bundle.js” file. What’s happening is that I’ve configured our application to use Webpack to bundle the 3 transpiled JavaScript files into 1 final concatenated JavaScript file. But Webpack does more than just concatenate the “module1.js”, “module2.js”, and “app.js” files together. The magic behind it is that it will look at the entry point of our application (which is app.js) and see that we’re importing other modules. It will then go through each import statement and recursively make the code in those imports available. So in our example, when Webpack is analyzing “app.js” and sees the import statement for “module1.js”, it will make all the code from module1.js available within app.js AND if it sees that module1.js also imports other files, it will recursively make all the code from that module available as well. Once it’s finished importing module1.js, Webpack sees the import statement for module2.js and continues the same recursive process. What’s amazing about this is that we now no longer have to worry about HOW to manage the module dependencies — we just have to DECLARE which modules depend on each other and then Webpack will handle the unraveling for us.

At this point, if you load up your browser and check out the index.html file, you should see the same console logs as the previous scenario. To summarize, by making the simple changes in our ideal scenario, we don’t have to worry about: