Menu

Módulos em TypeScript: import, export e import type

Todo arquivo TypeScript com um import ou export no nível superior é um módulo. Veja exports nomeados e default, import type e export type, como a opção module decide entre saída ES module e CommonJS e por que node16 e nodenext exigem a extensão .js nos imports.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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);
FormaO 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: true mantém todo import que não está marcado com type, 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 .ts diretamente no Node (type stripping) remove as anotações de tipo, mas não olha para outros arquivos. Um import { Point } sem marcação fica no código, e o Node falha em tempo de execução com SyntaxError: 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 em node_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 exports do seu package.json. Com a resolução node16, nodenext e bundler, 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:

ArquivoFormato de saída
.ts em um pacote com "type": "module"ES module
.ts em um pacote sem issoCommonJS
.mtssempre ES module, gerado como .mjs
.ctssempre 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" (com module definido como esnext, preserve ou commonjs) aceita ./math sem extensão, já que o bundler faz a resolução.
  • rewriteRelativeImportExtensions: true permite escrever ./math.ts, o mesmo caminho que funciona quando o Node roda o arquivo .ts diretamente, e o reescreve como ./math.js na 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.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR