Um módulo TypeScript é um arquivo com pelo menos um import ou export no nível superior. Tudo o que é declarado nele é privado ao arquivo, a menos que você faça export, e os outros arquivos fazem import do que precisam. A sintaxe é a de ES modules do JavaScript mais algumas formas exclusivas para tipos.
Exports nomeados e default
Um projeto tem muitos arquivos, então o lado que importa fica assim. O export default é importado sem chaves e pode receber qualquer nome; exports nomeados vão entre chaves e mantêm seus nomes, a menos que você os renomeie com as.
// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js"; // everything, as one object
const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
| Forma | O que ela importa |
|---|---|
import { a, b } from "./m.js" | os exports nomeados a e b |
import x from "./m.js" | o export default, com o nome x |
import * as m from "./m.js" | um objeto namespace com todos os exports |
import { a as b } from "./m.js" | a, renomeado para b neste arquivo |
import type { T } from "./m.js" | só tipos, removidos da saída |
import "./setup.js" | executa o arquivo pelos efeitos colaterais |
export { a } from "./m.js" | reexporta a sem importá-lo |
export * from "./m.js" | reexporta todos os exports nomeados |
Reexports permitem que um arquivo (muitas vezes index.ts) reúna a API pública de uma pasta. O comportamento de módulos do JavaScript puro, como live bindings e cache de módulos, está em ES modules.
import type e export type
Tipos não existem em tempo de execução, então um import usado só como tipo não tem nada para carregar. import type deixa isso explícito, e a instrução some da saída JavaScript. O modificador type também funciona em um único nome dentro de um import comum.
import type { User } from "./models.js"; // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives
export type { User }; // re-export a type only
export type UserId = User["id"];
Sem a palavra-chave, o TypeScript ainda remove os nomes que ele consegue ver que são só tipos. Duas configurações tornam a palavra-chave obrigatória:
verbatimModuleSyntax: truemantém todo import que não está marcado comtype, então um import de tipo sem a marcação é o erro TS1484:'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.- Rodar arquivos
.tsdiretamente no Node (type stripping) remove as anotações de tipo, mas não olha para outros arquivos. Umimport { Point }sem marcação fica no código, e o Node falha em tempo de execução comSyntaxError: The requested module './math.ts' does not provide an export named 'Point'.
Escrever type em todo import que é só de tipo funciona nos dois casos, então é o hábito a criar.
Cannot find module
Quando o caminho de um import não leva a um arquivo nem a um pacote com tipos, o compilador para com o erro TS2307. Rode este para ver:
A saída é index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations. As causas mais comuns:
- Um erro de digitação em um caminho relativo, ou um
./faltando (sem ele, o nome é procurado emnode_modules). - Um pacote JavaScript sem tipos incluídos: instale
@types/{package}se existir, ou escreva um arquivo de declaração para ele. - Um subcaminho que o pacote não lista no campo
exportsdo seupackage.json. Com a resoluçãonode16,nodenextebundler, só os pontos de entrada listados podem ser importados.
Saída em ES modules ou CommonJS
Você sempre escreve import e export. A opção module no tsconfig.json decide qual JavaScript sai.
// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));
Com node16, node18, node20 ou nodenext, o TypeScript segue as regras do próprio Node para cada arquivo:
| Arquivo | Formato de saída |
|---|---|
.ts em um pacote com "type": "module" | ES module |
.ts em um pacote sem isso | CommonJS |
.mts | sempre ES module, gerado como .mjs |
.cts | sempre CommonJS, gerado como .cjs |
Com module: "esnext" ou "preserve", a saída mantém import/export, que é o que bundlers como Vite e esbuild esperam. As diferenças entre os dois sistemas de módulos em tempo de execução estão em CommonJS vs ESM.
Extensões de arquivo nos imports
Com module: node16 ou nodenext, um arquivo ES module precisa indicar o arquivo que o Node realmente vai carregar, e esse é o .js compilado. O TypeScript mapeia ./math.js de volta para math.ts na verificação de tipos.
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
O texto do erro é Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'? Arquivos CommonJS com as mesmas configurações podem omitir a extensão, porque o require testa as extensões sozinho.
Duas outras configurações mudam a regra:
moduleResolution: "bundler"(commoduledefinido comoesnext,preserveoucommonjs) aceita./mathsem extensão, já que o bundler faz a resolução.rewriteRelativeImportExtensions: truepermite escrever./math.ts, o mesmo caminho que funciona quando o Node roda o arquivo.tsdiretamente, e o reescreve como./math.jsna saída.
Resolução de módulos e paths
Para um nome sem caminho como "zod", o TypeScript procura em node_modules, lê o package.json do pacote (os campos exports e types) e, se não achar, recorre a node_modules/@types/zod. Nomes relativos (./, ../) são resolvidos a partir do arquivo que importa. Também dá para importar um arquivo .json; as configurações necessárias estão na página de JSON.
paths no tsconfig.json adiciona aliases para as suas próprias pastas:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths só afeta a verificação de tipos. O arquivo gerado continua dizendo import { v } from "@lib/util", então outra coisa precisa resolver isso em tempo de execução: um bundler configurado com o mesmo alias, ou o campo imports do package.json no Node (aliases que começam com #, como "#lib/*": "./dist/lib/*"), que funciona sem nenhuma ferramenta extra. baseUrl não existe mais no TypeScript 7 (erro TS5102); escreva as entradas de paths relativas ao tsconfig.json, começando com ./.
Perguntas frequentes
Qual é a diferença entre import e import type no TypeScript?
import type { User } from "./user.js" só pode trazer tipos, e a instrução inteira é removida da saída JavaScript. Um import comum pode trazer valores e tipos; o TypeScript descarta os nomes usados só como tipos, mas com verbatimModuleSyntax ele exige que você os marque com type para que a saída seja exatamente o que você escreveu.
Por que o TypeScript exige .js nos caminhos de import?
Com module: node16 ou nodenext, um arquivo ES module precisa importar usando o nome real do arquivo que o Node vai carregar em tempo de execução, e esse arquivo é o .js compilado. O TypeScript resolve ./math.js para math.ts durante a verificação de tipos. Omitir a extensão em um arquivo ES module é o erro TS2835.
Devo usar export default ou exports nomeados no TypeScript?
Os dois funcionam. Muitos times preferem exports nomeados: o nome é o mesmo em todo arquivo que importa, os editores fazem o auto-import com confiança e renomear vira um refactor em vez de uma busca. Um export default deixa cada arquivo que importa escolher o próprio nome.
A opção paths do tsconfig muda o import na saída?
Não. paths só diz ao verificador de tipos onde encontrar um módulo. O JavaScript gerado mantém "@lib/util" como foi escrito, então um bundler, ou o campo imports do package.json no Node, precisa resolvê-lo em tempo de execução.
Como corrigir "Cannot find module" no TypeScript?
O erro TS2307 significa que o caminho não aponta para um arquivo que o TypeScript encontra, ou que um pacote não traz tipos. Confira o caminho relativo e a extensão, instale o pacote @types/... se o pacote não tiver tipos próprios, ou escreva um pequeno arquivo de declaração para ele.