Modules in TypeScript

Introduction

TypeScript uses the same module syntax as modern JavaScript: import and export. The compiler checks types across files, emits JavaScript modules according to tsconfig, and can interoperate with CommonJS in Node. This chapter covers organizing .ts files, export styles, and how TypeScript treats modules differently from plain JS.

Prerequisites

One File, One Module

Each .ts file with top-level import or export is its own module. Files without them are global scripts (avoid in apps—use modules).

text
src/
  math.ts
  app.ts

src/math.ts:

typescript
export function add(a: number, b: number): number {
  return a + b;
}
 
export const PI = 3.14159;

src/app.ts:

typescript
import { add, PI } from "./math.js";
 
const total = add(2, 3);
console.log(total, PI);

Code explanation:

  • ./math.js extension in import is common with moduleResolution: "NodeNext"—TypeScript resolves to math.ts at compile time
  • Types of add and PI flow into app.ts automatically

Tip

Extension in Imports

With Node16/NodeNext, use .js in import paths for emitted ESM even though source is .ts. Bundlers may allow extensionless paths—follow your toolchain docs.

Named and Default Exports

Named (multiple per file):

typescript
export interface User {
  id: number;
  name: string;
}
 
export function formatUser(user: User): string {
  return `${user.name} (#${user.id})`;
}

Default (one primary export):

typescript
export default class Logger {
  log(message: string): void {
    console.log(message);
  }
}

Import styles:

typescript
import Logger from "./logger.js";
import { User, formatUser } from "./user.js";

Re-exports

Barrel file pattern:

typescript
// src/models/index.ts
export { User, formatUser } from "./user.js";
export type { User as UserModel } from "./user.js";

Code explanation:

  • export type re-exports only types (erased at runtime with verbatimModuleSyntax / importsNotUsedAsValues settings)
  • Barrels simplify imports but can hurt tree-shaking if overused

import type and export type

Type-only imports never emit runtime requires:

typescript
import type { User } from "./user.js";
 
function demo(user: User): void {
  console.log(user.name);
}
typescript
export type { User };
export { formatUser };

Keeps bundles clean when verbatimModuleSyntax is enabled.

CommonJS Interop (Node)

Legacy Node code uses require / module.exports. TypeScript can emit or consume CommonJS depending on module in tsconfig.

ESM import from a CJS package (typical modern setup):

typescript
import pkg from "some-cjs-package";

With esModuleInterop: true, default import often works for module.exports = ....

Writing CJS-style in .ts (when configured):

typescript
import fs = require("node:fs");
 
export = {
  read(): string {
    return fs.readFileSync("/tmp/demo.txt", "utf8");
  },
};

export = targets CommonJS module.exports. Prefer ESM for new projects.

Dynamic import

typescript
async function loadLocale(code: string): Promise<{ greet: (name: string) => string }> {
  const module = await import(`./locales/${code}.js`);
  return module.default;
}

Returns a Promise of the module namespace; types can be asserted or declared.

Resolution Hints

TypeScript resolves imports using moduleResolution (next chapter). Errors like “Cannot find module” usually mean:

  • Wrong path or missing .js suffix for NodeNext
  • Package not installed
  • Missing types for JS package (@types/...)

Practical Layout

text
src/
  index.ts          # entry
  lib/
    math.ts
  models/
    user.ts
    index.ts        # optional barrel

src/index.ts:

typescript
import { add } from "./lib/math.js";
import { formatUser } from "./models/user.js";
 
console.log(add(1, 2));
console.log(formatUser({ id: 1, name: "Ada" }));

FAQ

.ts import vs .js import?

Source is .ts; emitted file is .js. NodeNext ESM expects import specifiers that match runtime files.

Can I use require in TypeScript?

Possible with certain module settings; ESM is the default for new Node and frontend projects.

Are types exported at runtime?

No. Interfaces and type aliases disappear. Values (class, function, const) remain.

Circular imports?

TypeScript allows them but they cause runtime issues if values are used before initialization. Refactor shared types to a third file.

import.meta?

Supported in ESM for import.meta.url etc., with appropriate module target.

Same as JavaScript modules?

Syntax matches JS. TypeScript adds type checking and import type / export type.