-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
163: Library Structures: Global, Modular, UMD
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): booleanthat returns true if the string contains an "@" symbol. - Create a function
isStrongPassword(pass: string): booleanthat 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
ValiToolnamespace. - Ensure that the TypeScript compiler doesn't complain about the
windowormoduleobjects (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.
There are no comments for now.