TypeScript
Completed
-
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
157: CommonJS Interop
I've seen this happen to almost every developer transitioning a project to TypeScript: you import a well-known library or a legacy utility file, the types look perfect in your IDE, but the moment you run the code, it crashes with a TypeError: ... is not a function.
Let's look at a classic example. Imagine you have a legacy utility file written in CommonJS (the old Node.js standard) that you aren't ready to rewrite yet:
// utils.js (CommonJS)
module.exports = {
formatDate: (date) => date.toISOString().split('T')[0],
slugify: (text) => text.toLowerCase().replace(/\s+/g, '-')
};
Now, you try to use it in your TypeScript file using the modern ES Module syntax:
// app.ts
import utils from './utils';
console.log(utils.formatDate(new Date()));
// ❌ Runtime Error: TypeError: utils.formatDate is not a function
Wait, why? If you hover over utils in VS Code, it probably looks fine. But at runtime, TypeScript (or rather, the compiled JS) is treating the CommonJS module.exports object as the entire module, not as the default export. In reality, the utils variable ends up being { default: { formatDate: ..., slugify: ... } } or something similar depending on your target, which is why calling .formatDate() directly on it fails.
Solving the Default Import Mismatch
The "correct" way to handle this in modern TypeScript is to let the compiler handle the glue code for you. You do this in your tsconfig.json by enabling esModuleInterop.
// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
When esModuleInterop is true, TypeScript emits a small helper function that checks if the imported module is a CommonJS module. If it is, it wraps it in a way that makes it behave like an ES module with a default export. Now, that import utils from './utils' line will actually work at runtime.
I usually recommend keeping esModuleInterop enabled by default in every project. It saves you from having to memorize which libraries use module.exports and which use export default.
Handling Modules Without Defaults
Sometimes you're working in a strict environment where you can't change the tsconfig, or you're dealing with a module that doesn't play nice with default imports. In those cases, you should use the namespace import syntax.
// Instead of: import utils from './utils';
import * as utils from './utils';
console.log(utils.formatDate(new Date())); // ✅ Works!
Using import * as ... tells TypeScript to take everything exported by the CommonJS module and put it into a single object. It's a more explicit way of saying, "I know this module exports a bunch of stuff, and I want all of it under this name."
Just a heads-up: if you use import * as utils, you can't use esModuleInterop's magic to treat it as a single default object. You're essentially opting into the "everything is a named export" mental model, which is often safer when you're unsure how a legacy package was bundled.
📋 Practical Task
Fixing the Legacy Config Import Crash
You are working on a project that uses a legacy settings.js file. The file uses CommonJS module.exports to export a configuration object. Your TypeScript code is currently crashing because it's trying to use a default import on a CommonJS module without the proper compiler settings.
The Legacy File (do not change):
// settings.js
module.exports = {
apiEndpoint: "https://api.example.com",
timeout: 5000
};
The Broken TypeScript File:
// configManager.ts
import settings from './settings';
export function getEndpoint() {
return settings.apiEndpoint;
}
Your Task:
- Modify the
configManager.tsfile to use a Namespace Import (import * as ...) so that the code works regardless of thetsconfig.jsonsettings. - Ensure the
getEndpointfunction still correctly returns theapiEndpointfrom the imported settings.
There are no comments for now.