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

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:

  1. Modify the configManager.ts file to use a Namespace Import (import * as ...) so that the code works regardless of the tsconfig.json settings.
  2. Ensure the getEndpoint function still correctly returns the apiEndpoint from the imported settings.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.