A Tale of Two Module Systems
To understand how TypeScript works with JavaScript modules, you need to know the two main module systems:
- CommonJS (CJS): The traditional system used by Node.js. It uses require() to import and module.exports to export.
- ES Modules (ESM): The modern standard built into the JavaScript language. It uses import and export statements.
TypeScript uses the ES Module syntax (import/export). The TypeScript compiler can then convert this syntax into different formats (like CommonJS) depending on your tsconfig.json settings.
The module option in tsconfig.json controls the output format:
JSON
{
  "compilerOptions": {
    "module": "CommonJS" // or "ESNext", "UMD", etc.
  }
}
What are Declaration Files (.d.ts)?
When you import a plain JavaScript library into your TypeScript project, the TS compiler has no idea what functions, variables, or classes that library contains. It can't provide type checking or autocomplete.
A declaration file, which ends in .d.ts, solves this. It's like a header file in other languages. It contains only type information—no implementations or logic. It describes the "shape" of the JavaScript code to the compiler.
For example, a simple math.js file:
JavaScript
// math.js
module.exports.add = function(a, b) {
  return a + b;
}
Could have a corresponding math.d.ts file:
TypeScript
// math.d.ts export function add(a: number, b: number): number;
Now, when you import { add } from './math'; in a .ts file, TypeScript will read the .d.ts file and know that add is a function that takes two numbers and returns a number.
Using JavaScript Libraries: The @types Namespace
Thankfully, you rarely have to write these declaration files yourself for popular libraries. The TypeScript community maintains a massive repository of high-quality declaration files for thousands of JavaScript packages on a project called DefinitelyTyped.
You can install these type definitions directly from npm. They are always published under the @types/ scope.
For example, to use the popular lodash library in a TypeScript project:
1. Install the library itself:
Bash
npm install lodash
2. Install its corresponding types:
Bash
npm install --save-dev @types/lodash
That's it! Now you can import and use lodash in your TypeScript code with full type safety and autocompletion.
TypeScript
import _ from 'lodash'; const numbers = [1, 2, 3, 4]; const shuffled = _.shuffle(numbers); // 'shuffled' is correctly typed as number[] // _.chunk(numbers, "hello"); // ERROR: Argument of type 'string' is not assignable to 'number'.
Many modern libraries are now written in TypeScript and ship with their own declaration files, so the second step is often not even necessary.
Creating Your Own Declaration File
If you're working with a small, internal JavaScript module or a library without a @types package, you can declare a module yourself. Create a .d.ts file and use the declare module 'module-name' syntax:
TypeScript
// custom-types.d.ts
declare module 'untyped-color-library' {
  export function hexToRgb(hex: string): { r: number, g: number, b: number };
  export function rgbToHex(r: number, g: number, b: number): string;
}
TypeScript will now automatically pick up this declaration file and apply these types whenever you import from 'untyped-color-library'.