TypeScript course menu
TypeScript Modules: import, export and import type
Learn how modules work in TypeScript: exporting and importing types and values, import type and type-only exports, verbatimModuleSyntax, file extensions in imports with nodenext, path aliases, and organising types across a project.
On this page
TypeScript uses the same ES module syntax as JavaScript (Modules): export and import. The difference is that you can also export and import types, and TypeScript has a few extra rules about how imports are written.
Exporting types and values#
export interface Course {
id: string;
title: string;
lessons: number;
}
export type Level = "beginner" | "intermediate" | "advanced";
export function describe(course: Course, level: Level): string {
return `${course.title} (${level}, ${course.lessons} lessons)`;
}import { describe, type Course, type Level } from "./models.js";
const ts: Course = { id: "ts", title: "TypeScript", lessons: 39 };
const level: Level = "intermediate";
console.log(describe(ts, level)); // TypeScript (intermediate, 39 lessons)Types and interfaces are exported and imported just like functions. At runtime, the type exports simply disappear.
import type#
Writing type before an imported name (or import type { ... } for the whole statement) marks it as type-only:
import type { Course, Level } from "./models.js"; // the whole import is types
import { describe, type Course } from "./models.js"; // mixed: one value, one typeType-only imports are guaranteed to be removed from the compiled JavaScript. This matters because tools that compile files one at a time, or that only strip types (esbuild, SWC, Node.js type stripping), cannot always tell whether an import is a type. If a type import were left in the output, the program could fail at runtime looking for an export that does not exist.
The verbatimModuleSyntax compiler option makes this explicit: imports without type are always kept, and the compiler reports an error when you import a type without marking it. It is recommended for modern projects (tsconfig.json).
File extensions in imports#
With "module": "nodenext" (used for Node.js projects), relative imports must include the extension of the output file, which is .js, even though the source file is .ts:
import { describe } from "./models.js"; // correct: models.ts compiles to models.js
import { describe } from "./models"; // error under nodenextThis looks odd at first, but it matches exactly what Node.js will load at runtime. Projects using a bundler (Vite, webpack, Next.js) usually set "moduleResolution": "bundler", which allows extensionless imports because the bundler resolves them.
Importing packages#
Packages installed with npm are imported by name. Many include their own type definitions; for others, types come from separate @types/... packages (Declaration Files):
import { readFile } from "node:fs/promises"; // types from @types/node
import path from "node:path";
console.log(typeof readFile, path.extname("lesson.ts"));function .tsPath aliases#
Long relative paths like ../../../components/Button.js are hard to read. The paths compiler option defines aliases:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}import { Button } from "@/components/Button";paths only tells TypeScript how to resolve the alias; your bundler or runtime must be configured to resolve it the same way. Frameworks such as Next.js set up the @/* alias for you. (This website uses exactly this alias.)
Organising types#
- Keep types next to the code that uses them when only one module needs them.
- Put types shared across the app in a dedicated module, such as
src/types/ormodels.ts. - Derive types from a single source where possible (
ReturnType,typeof, schema validators) instead of writing the same shape twice. - Export types that callers need: if a function's parameter type is not exported, users of your module cannot name it.
Practice#
Practice exercise
Split this code into shapes.ts (the Shape type and the area function, both exported) and main.ts (which imports them, using a type-only import for Shape):
type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
function area(s: Shape): number { return s.kind === "circle" ? Math.PI * s.r ** 2 : s.side ** 2; }
const shapes: Shape[] = [{ kind: "square", side: 3 }];
console.log(shapes.map(area));Show solution
export type Shape = { kind: "circle"; r: number } | { kind: "square"; side: number };
export function area(s: Shape): number {
return s.kind === "circle" ? Math.PI * s.r ** 2 : s.side ** 2;
}import { area, type Shape } from "./shapes.js";
const shapes: Shape[] = [{ kind: "square", side: 3 }];
console.log(shapes.map(area)); // [ 9 ]Key takeaway#
Finished this lesson?