← TypeScript: tipos, contratos e diagnóstico
09 / 10 · 60 MIN

Módulos, declarações e contratos do ambiente

Liga os contratos de tipos aos módulos e implementações que o consumidor realmente recebe.

O módulo define uma fronteira pública

Num ficheiro tratado como módulo, uma declaração local não fica automaticamente disponível aos consumidores. export identifica os nomes públicos; import permite usar os nomes que outro módulo exporta. const impede reatribuir uma variável, mas não a torna pública. Um módulo pode ter exports nomeados sem fornecer export default.

Define a fronteira pelo que o consumidor precisa de fazer. Uma função de criação pode ser pública enquanto detalhes de armazenamento ficam locais. Mudar um nome exportado ou retirar um valor usado externamente é uma alteração de contrato, mesmo que todas as utilizações dentro do próprio ficheiro continuem corretas.

Tipos e valores não têm o mesmo ciclo de vida

import type comunica que a ligação serve apenas a análise de tipos. A instrução inteira desaparece do JavaScript emitido e não fornece um valor chamável. Uma função exportada pode ser importada como valor para ser chamada; marcá-la apenas como tipo e depois tentar chamá-la não respeita esse contrato.

Com verbatimModuleSyntax, distingue explicitamente os imports de tipos dos imports de valores. No contexto ESM do exemplo, o import de createJob mantém-se e o import type de Job é removido. Isto não transforma Job num validador de dados nem autoriza presumir que argumentos externos foram inspecionados.

Efeitos de carregamento precisam de uma dependência

Um módulo pode registar um handler quando é carregado. Se o consumidor só mantém import type de uma interface desse módulo, esse carregamento não é causado pela instrução apagada. Quando o registo é realmente necessário, um import explícito para efeito, como import "./register.js", comunica a dependência de execução.

Revê também a alternativa de expor uma função de registo e a chamar num ponto controlado. A escolha depende do contrato da aplicação, mas nenhuma das soluções deve depender da presença acidental de uma anotação de tipo. Um import de tipo adicional não prova que uma inicialização ocorreu.

O host decide como encontra a implementação

O exemplo usa module e moduleResolution nodenext, ficheiros .ts num pacote com type: module e execução direta dos ficheiros .js emitidos em Node ESM. Nesse contexto, ./model.js identifica o ficheiro relativo final. A resolução de TypeScript pode procurar a origem model.ts através da substituição de extensões.

Não generalizes esta regra para todos os hosts. Um bundler pode aceitar caminhos sem extensão e transformá-los, enquanto a execução direta em Node ESM exige extensões nos specifiers relativos. .mts e .cts também tornam explícita a família de formato. Escolhe a configuração a partir do ambiente real e confirma o contrato dos ficheiros distribuídos.

paths não publica o resolvedor privado

paths ajuda o compilador a interpretar nomes durante a verificação. Não reescreve automaticamente esses nomes no JavaScript emitido. Se @app/model só existe na configuração privada do repositório, um consumidor que recebe esse import pode falhar antes de conseguir chamar qualquer função. Um .d.ts adicional não ensina o host a resolver o alias.

Uma biblioteca distribuída precisa de specifiers relativos ou de pacotes que o consumidor consiga resolver, ou de uma etapa de transformação assumida explicitamente no produto final. Revê tanto o JavaScript como as declarações distribuídas. Corrigir apenas os imports da implementação pode deixar o consumidor de tipos dependente do mesmo alias privado.

Declarações descrevem uma implementação

Um ficheiro .d.ts fornece informação para a verificação e não gera o corpo JavaScript de uma função declarada. Antes de procurar tipos separados para um pacote, verifica se a própria distribuição já inclui declarações. A versão das declarações deve corresponder à versão efetivamente entregue, incluindo retornos assíncronos, nomes e formas exportadas.

A forma abreviada declare module "legacy-client" pode remover a ausência de tipos, mas deixa o consumidor sem um contrato detalhado. any pode então ocultar chamadas incompatíveis. Se uma declaração promete Promise<void> e a implementação inicia trabalho sem devolver conclusão, a anotação não torna await suficiente para esperar por esse trabalho.

Rever o exemplo desde a origem até ao consumidor

Lê model.ts e app.ts como dois ficheiros. createJob constrói o valor real e Job descreve a forma do retorno. prepareJob depende do import de valor para poder chamar createJob. Os dois ficheiros emitidos devem manter uma relação de diretórios compatível com ./model.js. Nenhuma declaração isolada substitui model.js nessa entrega.

A assinatura recebe uma string já disponível ao chamador; não afirma que seja um identificador válido segundo regras externas. Uma fronteira HTTP ou de configuração continua a precisar da validação ensinada na aula anterior. A revisão de módulos acrescenta disponibilidade e resolução ao contrato de dados, sem dispensar esse contrato.

// Separate files; TypeScript 7.0, strict and verbatimModuleSyntax enabled.
// module/moduleResolution: nodenext; package.json contains "type": "module".
// src/model.ts
export type Job = { id: string; attempts: number };
export function createJob(id: string): Job {
  return { id, attempts: 0 };
}

// src/app.ts
import type { Job } from "./model.js";
import { createJob } from "./model.js";
export function prepareJob(id: string): Job {
  return createJob(id);
}
NA PRÁTICA

Lê os dois ficheiros separadamente. Job só é necessário na verificação; createJob é necessário quando prepareJob é chamada. O specifier ./model.js corresponde ao ficheiro emitido, embora a origem seja model.ts. O exemplo não valida um identificador externo.

Armadilhas comuns

Confundir .d.ts com implementação; confiar que paths reescreve imports; retirar um import que sustentava um efeito; usar a resolução de um bundler num host que não a implementa.

Tópicos relacionados: Funções e contratos de chamada · Dados externos e fronteiras de erro · Configuração, entrega e migração

Leva esta ideia contigo

Um contrato público precisa de tipos fiéis, valores disponíveis e nomes que o consumidor consiga resolver. Revê os três em conjunto.

Criar conta

Referência: TypeScript: how javascript modules are defined · TypeScript 7.0; explicit strict and host configuration