Last 30 Days
No notifications
Every .ts file with an import or export is a module. TypeScript layers a type-aware import system on top of standard ES modules: import type, declaration files (.d.ts), ambient modules, and the @types/* ecosystem. Knowing how the pieces fit makes integrating any JS library painless.
Any file containing top-level import or export is a module; everything else is a *script* (its declarations leak into the global scope — usually not what you want).
// math.ts
export function add(a: number, b: number) { return a + b; }
export const PI = 3.14;// app.ts
import { add, PI } from "./math";
Default vs named:
// logger.ts
export default function log(msg: string) { console.log(msg); }
export const LEVEL = "info";// app.ts
import log, { LEVEL } from "./logger";
import type and export typeIf you only need a value's *type* (no runtime use), use import type so the bundler can drop the import entirely.
import type { User } from "./user"; // erased at compile time
import { saveUser } from "./user"; // kept (runtime use)You can mix them inline:
import { saveUser, type User } from "./user";This matters a lot for isolatedModules + ESM bundlers (Vite, esbuild, swc) — they don't see across files, so they need explicit type-only imports.
tsconfig essentials for modules| Option | What it does |
module | Output module syntax (ESNext, CommonJS, NodeNext) |
moduleResolution | How imports are resolved — use Bundler for Vite/Next, NodeNext for Node |
esModuleInterop | Lets you do import express from "express" for CJS modules |
isolatedModules | Treat every file independently — required by Vite/esbuild |
paths | Path aliases (the @/ you use in this project) |
baseUrl | Required for paths |
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}Now import { x } from "@/lib/x" works from anywhere.
.d.tsA .d.ts file contains types only, no runtime code. Three places they come from:
{ "compilerOptions": { "declaration": true } }tsc emits a .d.ts next to every .js. Essential when publishing a library.
Most modern libraries (e.g. zod, react, next) ship .d.ts inside their npm package. tsc picks them up automatically — you do nothing.
@types/*For libraries written in plain JS (e.g. lodash, express, node), types live on DefinitelyTyped:
npm i lodash
npm i -D @types/lodashIf you've ever wondered why @types/node shows up in basically every TS project — that's why.
declareTell TS that something exists at runtime even though there's no import:
// global.d.ts
declare const __APP_VERSION__: string; // injected by Vite define
declare module "*.svg" { // import logo from "./logo.svg"
const content: string;
export default content;
}Common patterns:
.svg, .css, .md).window or Express Request.// types/express.d.ts
import "express";
declare module "express" {
interface Request {
userId?: string;
}
}This is what middleware libraries do to add fields to Express's request.
// lib/index.ts
export * from "./math";
export * from "./logger";
export { default as Button } from "./Button";Now consumers do import { add, log, Button } from "@/lib". Watch out: in some bundlers, naive barrels can hurt tree-shaking — keep them small or skip them.
import()Asynchronous + code-split. Returns a Promise of the module's namespace object:
const { default: heavy } = await import("./heavyModule");In Next.js / React this is how you lazy-load components.
import type for type-only imports.paths for clean aliases (@/*).@types/ for any JS-only library..d.ts files to declare globals / non-TS imports / module augmentation.