Skip to Content
Course content

163: Library Structures: Global, Modular, UMD

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time: developers think that as long as the tsc compiler spits out a .js file, the distribution part is done. They assume that because their code is valid JavaScript, any project—whether it's a legacy jQuery site or a modern Vite-powered React app—will just "know" how to use it.

The "Just Compile It" Fallacy

Imagine you've written a handy utility called FinanceUtils that calculates compound interest. You write your TypeScript, run tsc, and get a finance.js file. You tell your teammate, "Just add a script tag to your HTML and call calculateInterest()."

// finance.ts
export function calculateInterest(principal: number, rate: number, time: number) {
    return principal * Math.pow(1 + rate, time);
}

When your teammate adds <script src="finance.js"></script> to their page, they get a ReferenceError: calculateInterest is not defined. Why? Because you used export. The compiler turned that into an ES Module. In a browser, a standard script tag doesn't "unwrap" an ES module into the global scope; it just executes the code. Since calculateInterest was exported and not assigned to the window object, it's effectively invisible to the rest of the page. I've spent way too many hours debugging this for junior devs who think the compiler handles the "delivery method" automatically.

Global Scope: The Legacy Way

If you're building a library specifically for people who just want to drop a <script> tag into a page without a build step, you're looking at a Global structure. In this world, you don't use export. Instead, you explicitly attach your functionality to the global object (window in browsers, global in Node).

// finance-global.ts
(function() {
    const FinanceUtils = {
        calculateInterest: (p: number, r: number, t: number) => p * Math.pow(1 + r, t)
    };
    (window as any).FinanceUtils = FinanceUtils;
})();

It's simple, but it's dangerous. You're polluting the global namespace. If someone else defines a FinanceUtils variable, one of you is getting overwritten. We generally avoid this unless we're targeting very old environments.

Modular: ESM and CommonJS

This is where most of us live now. You use export and import. However, there's a catch: the JavaScript ecosystem is split between ESM (ES Modules) and CommonJS (the require/module.exports pattern used by Node.js).

If you target module: "CommonJS" in your tsconfig.json, your code becomes compatible with older Node versions. If you target "ESNext", you're using the modern standard. The problem? A library that is only CommonJS will frustrate a developer using a pure ESM project, and vice versa. I usually recommend targeting ESM for the web, but if you're publishing to NPM, you'll likely need to provide both.

UMD: The Universal Bridge

Universal Module Definition (UMD) is essentially a pattern that asks the environment: "Who are you?" It checks if define exists (for AMD loaders like RequireJS), then checks if module.exports exists (for CommonJS), and finally falls back to attaching the library to the global window object.

You won't usually write the UMD wrapper by hand because it's a boilerplate nightmare. Instead, we use bundlers like Rollup or Webpack. You write your clean, modular TypeScript code, and the bundler wraps it in a function that looks something like this:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof module === 'object' && module.exports) {
        module.exports = factory();
    } else {
        root.FinanceUtils = factory();
    }
}(typeof self !== 'undefined' ? self : this, function () {
    return { calculateInterest: (p, r, t) => p * Math.pow(1 + r, t) };
}));

By distributing your library as UMD, you stop being the "bottleneck" for your users. Whether they are using a 2012 legacy codebase or a 2024 cutting-edge framework, your library just works.




📋 Practical Task

Exercise: Implementing a UMD-compatible Validation Library

You are tasked with creating a small validation library called ValiTool. The goal is to ensure that the library can be used in three different ways: via a global script tag, via require() in Node, and via import in a modern browser.

Requirements:

  • Create a function isValidEmail(email: string): boolean that returns true if the string contains an "@" symbol.
  • Create a function isStrongPassword(pass: string): boolean that returns true if the password is at least 8 characters long.
  • Instead of using a bundler for this exercise, manually implement the UMD wrapper pattern described in the lesson to export these two functions under the ValiTool namespace.
  • Ensure that the TypeScript compiler doesn't complain about the window or module objects (use type casting like (window as any) where necessary).

Verification: Your final code should be a single file that, when compiled to JS and run in a browser console, allows you to call ValiTool.isValidEmail("test@example.com") without any import statements.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.