<h2>Introdução à TypeScript Compiler API</h2>
<p>A TypeScript Compiler API é um conjunto de interfaces e funções que permitem interagir programaticamente com o compilador TypeScript. Diferentemente de usar o <code>tsc</code> via linha de comando, a API oferece controle fino sobre como o código é parseado, transformado e gerado. Isso abre possibilidades para criar ferramentas de análise estática, geradores de código, refatoradores e linters customizados.</p>
<p>A maioria dos desenvolvedores nunca trabalha diretamente com essa API, mas ferramentas populares como ESLint, Prettier, NestJS e até IDEs modernas a utilizam internamente. Entender como funciona permite que você crie soluções sofisticadas que manipulam código TypeScript/JavaScript como dados estruturados, abrindo um mundo de automação e análise que seria impossível com abordagens baseadas em regex ou parsing manual.</p>
<h2>Conceitos Fundamentais</h2>
<h3>Abstract Syntax Tree (AST)</h3>
<p>Quando o compilador TypeScript processa código, ele não o vê como texto bruto. Primeiro, transforma-o em uma árvore de sintaxe abstrata (AST), onde cada elemento do código — variáveis, funções, tipos, expressões — torna-se um nó estruturado. Essa representação hierárquica é a base para tudo que faremos com a API.</p>
<p>Cada nó na AST tem um tipo específico (como <code>FunctionDeclaration</code>, <code>VariableStatement</code>, <code>InterfaceDeclaration</code>) e propriedades que descrevem seus detalhes. Compreender essa estrutura é essencial, pois você passará a maior parte do tempo navegando e inspecionando esses nós.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
const code = `
function greet(name: string): void {
console.log("Hello, " + name);
}
`;
// Criar um arquivo virtual de origem
const sourceFile = ts.createSourceFile(
'example.ts',
code,
ts.ScriptTarget.Latest,
true
);
// Função recursiva para inspecionar os nós
function visitNode(node: ts.Node, depth: number = 0) {
const indent = ' '.repeat(depth * 2);
console.log(${indent}${ts.SyntaxKind[node.kind]});
ts.forEachChild(node, child => visitNode(child, depth + 1));
}
visitNode(sourceFile);</code></pre>
<p>Este exemplo imprime a hierarquia da AST. Você verá <code>SourceFile</code>, depois <code>FunctionDeclaration</code>, depois seus filhos (<code>Identifier</code>, <code>Parameter</code>, <code>Block</code>, etc.). Essa estrutura é determinística e previsível.</p>
<h3>Visitantes e Transformadores</h3>
<p>A forma mais elegante de trabalhar com ASTs é através do padrão Visitor. TypeScript fornece funções como <code>forEachChild</code>, <code>visit</code> e <code>visitEachChild</code> que permitem percorrer a árvore. Além disso, a API oferece transformadores (transformers) que permitem modificar a AST de forma imutável, gerando uma nova árvore transformada.</p>
<p>Transformadores são especialmente poderosos porque mantêm a integridade da árvore — não modificam nós diretamente, mas retornam versões novas com as mudanças desejadas. Isso é importante para manter consistência e rastreabilidade.</p>
<h2>Parseando Código</h2>
<h3>Parseamento Básico</h3>
<p>O primeiro passo é converter código em texto para uma AST. A função <code>createSourceFile</code> é sua porta de entrada. Ela recebe o nome do arquivo, o conteúdo e configurações de destino.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
function parseTypeScriptCode(filePath: string, code: string): ts.SourceFile {
return ts.createSourceFile(
filePath,
code,
ts.ScriptTarget.ES2020,
true, // setParentNodes = true para ter acesso aos pais
);
}
// Exemplo de uso
const myCode = `
interface User {
id: number;
name: string;
email?: string;
}
const user: User = { id: 1, name: "Alice" };
`;
const ast = parseTypeScriptCode('models.ts', myCode);
console.log(Arquivo parseado com sucesso. Kind: ${ast.kind});</code></pre>
<p>O parâmetro <code>setParentNodes</code> é crucial — quando ativado, cada nó mantém uma referência ao seu pai na árvore, permitindo navegação bidirecional.</p>
<h3>Inspecionando Nós Específicos</h3>
<p>Depois de parsear, geralmente queremos encontrar nós de tipos específicos. Uma estratégia comum é criar um visitante que coleta informações.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
interface FunctionInfo {
name: string;
parameters: string[];
returnType?: string;
}
class FunctionCollector {
functions: FunctionInfo[] = [];
visit(node: ts.Node): void {
if (ts.isFunctionDeclaration(node)) {
const name = node.name?.getText() | | 'anonymous'; const parameters = node.parameters.map(p => p.name?.getText() || 'param');
const returnType = node.type?.getText();
this.functions.push({ name, parameters, returnType });
}
ts.forEachChild(node, child => this.visit(child));
}
collect(sourceFile: ts.SourceFile): FunctionInfo[] {
this.visit(sourceFile);
return this.functions;
}
}
// Uso
const code = `
function add(a: number, b: number): number {
return a + b;
}
async function fetchUser(id: string): Promise<User> {
// ...
}
`;
const sourceFile = ts.createSourceFile('math.ts', code, ts.ScriptTarget.Latest, true);
const collector = new FunctionCollector();
const functions = collector.collect(sourceFile);
console.log(JSON.stringify(functions, null, 2));
// Output:
// [
// { name: "add", parameters: ["a", "b"], returnType: "number" },
// { name: "fetchUser", parameters: ["id"], returnType: "Promise<User>" }
// ]</code></pre>
<p>Este padrão é a base para análise estática. Você define predicados (<code>isFunctionDeclaration</code>, <code>isInterfaceDeclaration</code>, etc.) para filtrar nós e extrair informações.</p>
<h2>Transformando Código</h2>
<h3>Transformadores Básicos</h3>
<p>A transformação de AST é onde a magia acontece. Em vez de trabalhar com strings, você modifica a estrutura do código programaticamente. A chave é usar <code>ts.visitEachChild</code> combinado com <code>ts.visitNode</code>, que preserva a estrutura enquanto permite substituições.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
// Transformador que adiciona um prefixo a todas as variáveis
const createPrefixTransformer = (prefix: string) => {
return (context: ts.TransformationContext) => {
const visit: ts.Visitor = (node: ts.Node): ts.Node => {
if (ts.isVariableDeclaration(node)) {
const newName = ts.factory.createIdentifier(prefix + node.name.getText());
return ts.factory.updateVariableDeclaration(
node,
newName,
node.exclamationToken,
node.type,
node.initializer,
);
}
return ts.visitEachChild(node, visit, context);
};
return (sourceFile: ts.SourceFile) => ts.visit(sourceFile, visit);
};
};
// Aplicar transformador
const code = `
let count: number = 0;
const message: string = "hello";
var flag = true;
`;
const sourceFile = ts.createSourceFile('vars.ts', code, ts.ScriptTarget.Latest, true);
const result = ts.transform(sourceFile, [createPrefixTransformer('$')]);
const transformed = result.transformed[0];
// Verificar resultado
function printAST(node: ts.Node): string {
if (ts.isVariableDeclaration(node)) {
return node.name.getText();
}
return '';
}
ts.forEachChild(transformed, child => {
ts.forEachChild(child, decl => {
if (ts.isVariableDeclaration(decl)) {
console.log(Nome da variável: ${decl.name.getText()});
}
});
});</code></pre>
<p>Note que você <em>não</em> modifica nós diretamente. Em vez disso, usa <code>ts.factory</code> para criar novos nós com as alterações desejadas. O <code>ts.visitEachChild</code> navega pela árvore recursivamente, permitindo que você aplique transformações em múltiplos nós.</p>
<h3>Exemplo Prático: Adicionar Logs Automáticos</h3>
<p>Um caso de uso real é adicionar statements de log a funções automaticamente. Isso é útil para debugging ou auditoria.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
const createLogTransformer = () => {
return (context: ts.TransformationContext) => {
const visit: ts.Visitor = (node: ts.Node): ts.Node => {
if (ts.isFunctionDeclaration(node) && node.body) {
const functionName = node.name?.getText() || 'anonymous';
// Criar statement de log
const logStatement = ts.factory.createExpressionStatement(
ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier('console'),
'log',
),
undefined,
[ts.factory.createStringLiteral(Entering function: ${functionName})],
),
);
// Adicionar log como primeira statement do corpo
const newBody = ts.factory.updateBlock(
node.body,
[logStatement, ...node.body.statements],
);
return ts.factory.updateFunctionDeclaration(
node,
node.modifiers,
node.asteriskToken,
node.name,
node.typeParameters,
node.parameters,
node.type,
newBody,
);
}
return ts.visitEachChild(node, visit, context);
};
return (sourceFile: ts.SourceFile) => ts.visit(sourceFile, visit);
};
};
// Uso
const code = `
function calculate(x: number): number {
return x * 2;
}
function greet(name: string): void {
console.log("Hi, " + name);
}
`;
const sourceFile = ts.createSourceFile('app.ts', code, ts.ScriptTarget.Latest, true);
const result = ts.transform(sourceFile, [createLogTransformer()]);
const transformed = result.transformed[0];
// Gerar código (próxima seção)
const printer = ts.createPrinter();
const output = printer.printFile(transformed);
console.log(output);</code></pre>
<p>A saída será código com <code>console.log</code> adicionado automaticamente no início de cada função. Essa transformação é não-destrutiva — o original permanece intacto, e você obtém uma nova AST transformada.</p>
<h2>Gerando Código</h2>
<h3>Imprimindo a AST Transformada</h3>
<p>Depois de transformar a AST, você precisa convertê-la de volta para texto legível. A classe <code>Printer</code> do TypeScript faz exatamente isso, e é extremamente simples.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
// Assumindo que você tem uma sourceFile transformada
const sourceFile = ts.createSourceFile(
'example.ts',
'const x: number = 42;',
ts.ScriptTarget.Latest,
true,
);
const printer = ts.createPrinter();
const code = printer.printFile(sourceFile);
console.log(code);
// Output: const x: number = 42;</code></pre>
<p>O <code>Printer</code> é responsável pela formatação final. Ele respeita a configuração do compilador e gera código válido de TypeScript.</p>
<h3>Construindo Código do Zero</h3>
<p>Às vezes, você não está transformando código existente, mas gerando-o completamente do zero. Use <code>ts.factory</code> para criar uma AST inteira.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
// Gerar interface automaticamente
function generateInterface(name: string, fields: Record<string, string>): ts.InterfaceDeclaration {
const members = Object.entries(fields).map(([fieldName, fieldType]) =>
ts.factory.createPropertySignature(
undefined,
fieldName,
undefined,
ts.factory.createTypeReferenceNode(fieldType),
),
);
return ts.factory.createInterfaceDeclaration(
undefined,
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
name,
undefined,
undefined,
members,
);
}
// Gerar uma source file com a interface
function createSourceFileFromInterface(
interfaceName: string,
fields: Record<string, string>,
): ts.SourceFile {
const interfaceDecl = generateInterface(interfaceName, fields);
// Criar um arquivo virtual vazio e adicionar a interface
const sourceFile = ts.createSourceFile(
${interfaceName}.ts,
'',
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
// Usar o factory para criar um novo arquivo com o statement
const newSourceFile = ts.factory.updateSourceFile(sourceFile, [interfaceDecl]);
return newSourceFile;
}
// Uso
const generatedFile = createSourceFileFromInterface('Product', {
id: 'number',
name: 'string',
price: 'number',
inStock: 'boolean',
});
const printer = ts.createPrinter();
const output = printer.printFile(generatedFile);
console.log(output);
// Output:
// export interface Product {
// id: number;
// name: string;
// price: number;
// inStock: boolean;
// }</code></pre>
<p>Este padrão é usado por geradores de código como OpenAPI generators, ORMs e frameworks que criam tipos a partir de metadados externos.</p>
<h3>Salvando em Arquivo</h3>
<p>Para completar o ciclo, você geralmente salva o código gerado em disco.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
function saveGeneratedCode(
dirPath: string,
fileName: string,
sourceFile: ts.SourceFile,
): void {
const fullPath = path.join(dirPath, fileName);
// Criar diretório se não existir
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const printer = ts.createPrinter();
const code = printer.printFile(sourceFile);
fs.writeFileSync(fullPath, code, 'utf-8');
console.log(✓ Código gerado em: ${fullPath});
}
// Exemplo de uso
const generated = createSourceFileFromInterface('User', {
id: 'string',
email: 'string',
createdAt: 'Date',
});
saveGeneratedCode('./src/generated', 'User.ts', generated);</code></pre>
<h2>Caso de Uso Completo: Gerador de DTOs</h2>
<p>Para consolidar tudo que foi aprendido, vamos criar um gerador de DTOs (Data Transfer Objects) que lê interfaces e cria classes correspondentes com validação automática.</p>
<pre><code class="language-typescript">import * as ts from 'typescript';
import * as fs from 'fs';
interface PropertyDef {
name: string;
type: string;
optional: boolean;
}
class DTOGenerator {
private sourceFile: ts.SourceFile;
constructor(filePath: string, code: string) {
this.sourceFile = ts.createSourceFile(
filePath,
code,
ts.ScriptTarget.Latest,
true,
);
}
// Extrair interfaces do código
extractInterfaces(): Map<string, PropertyDef[]> {
const interfaces = new Map<string, PropertyDef[]>();
const visit = (node: ts.Node) => {
if (ts.isInterfaceDeclaration(node)) {
const interfaceName = node.name.getText();
const properties: PropertyDef[] = [];
node.members.forEach(member => {
if (ts.isPropertySignature(member)) {
const propName = member.name?.getText() | | ''; const propType = member.type?.getText() || 'any';
const isOptional = member.questionToken !== undefined;
properties.push({
name: propName,
type: propType,
optional: isOptional,
});
}
});
interfaces.set(interfaceName, properties);
}
ts.forEachChild(node, visit);
};
visit(this.sourceFile);
return interfaces;
}
// Gerar classe DTO para uma interface
generateDTOClass(interfaceName: string, properties: PropertyDef[]): ts.ClassDeclaration {
// Criar propriedades da classe
const classProperties = properties.map(prop =>
ts.factory.createPropertyDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.PublicKeyword)],
prop.name,
prop.optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
ts.factory.createTypeReferenceNode(prop.type),
undefined,
),
);
// Criar construtor
const constructorParameters = properties.map(prop =>
ts.factory.createParameterDeclaration(
undefined,
undefined,
prop.name,
prop.optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
ts.factory.createTypeReferenceNode(prop.type),
),
);
const constructorBody = ts.factory.createBlock(
properties.map(prop =>
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createThis(),
prop.name,
),
ts.SyntaxKind.EqualsToken,
ts.factory.createIdentifier(prop.name),
),
),
),
true,
);
const constructor = ts.factory.createConstructorDeclaration(
undefined,
constructorParameters,
constructorBody,
);
// Montar a classe
const dtoClassName = ${interfaceName}DTO;
return ts.factory.createClassDeclaration(
undefined,
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
dtoClassName,
undefined,
undefined,
[constructor, ...classProperties],
);
}
// Gerar múltiplas classes e combinar em um arquivo
generateDTOFile(): ts.SourceFile {
const interfaces = this.extractInterfaces();
const statements: ts.Statement[] = [];
interfaces.forEach((properties, interfaceName) => {
const dtoClass = this.generateDTOClass(interfaceName, properties);
statements.push(dtoClass);
});
const sourceFile = ts.createSourceFile(
'generated-dtos.ts',
'',
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
return ts.factory.updateSourceFile(sourceFile, statements);
}
print(): string {
const generatedFile = this.generateDTOFile();
const printer = ts.createPrinter();
return printer.printFile(generatedFile);
}
}
// Uso prático
const sourceCode = `
interface User {
id: string;
email: string;
name: string;
phone?: string;
}
interface Product {
sku: string;
title: string;
price: number;
description?: string;
}
`;
const generator = new DTOGenerator('models.ts', sourceCode);
const output = generator.print();
console.log(output);
// Salvar em arquivo
fs.writeFileSync('generated-dtos.ts', output, 'utf-8');</code></pre>
<p>Este exemplo demonstra o ciclo completo: parseamento de interfaces existentes, extração de informações, geração de novas classes e impressão de código válido. Esse padrão é usado em bibliotecas reais de geração de código.</p>
<h2>Conclusão</h2>
<p>Dominando a TypeScript Compiler API, você agora compreende como compiladores modernos funcionam internamente — não como caixas pretas, mas como sistemas estruturados que transformam texto em ASTs, manipulam essas árvores e regeneram código válido. Isso abre portas para criar ferramentas sofisticadas de análise estática, geradores de código e refatoradores que operam no nível semântico, não textual.</p>
<p>Os três pilares que consolidamos foram: primeiro, <strong>parseamento</strong> — converter código em estruturas de dados navegáveis; segundo, <strong>transformação</strong> — modificar ASTs de forma funcional e imutável usando o padrão Visitor; terceiro, <strong>geração</strong> — converter árvores de volta para código legível. Compreender esses três pilares permite que você construa qualquer ferramenta que necessite manipular TypeScript/JavaScript programaticamente.</p>
<p>A chave para evitar frustrações é lembrar que você não trabalha com strings, mas com objetos fortemente tipados. Sempre use as funções <code>isFunctionDeclaration</code>, <code>isVariableStatement</code>, etc., para validar tipos antes de acessar propriedades, e use <code>ts.factory</code> para criar novos nós em vez de tentar modificar os existentes.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API" target="_blank" rel="noopener noreferrer">TypeScript Compiler API Documentation</a></li>
<li><a href="https://ts-ast-viewer.com/" target="_blank" rel="noopener noreferrer">TypeScript AST Viewer - Ferramenta Interativa</a></li>
<li><a href="https://basarat.gitbook.io/typescript/" target="_blank" rel="noopener noreferrer">Basarat Ali Syed - TypeScript Deep Dive (Seção Compiler API)</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/compiler-options.html" target="_blank" rel="noopener noreferrer">Official TypeScript Handbook - Compiler Options</a></li>
<li><a href="https://github.com/itsdougsimmons/typescript-custom-transformers" target="_blank" rel="noopener noreferrer">Creating TypeScript Custom Transformers</a></li>
</ul>
<p><!-- FIM --></p>