TypeScript

TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática

16 min de leitura

TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática

Introdução à TypeScript Compiler API A TypeScript Compiler API é um conjunto de interfaces e funções que permitem interagir programaticamente com o compilador TypeScript. Diferentemente de usar o 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. 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. Conceitos Fundamentais Abstract Syntax Tree (AST) 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

<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 &#039;typescript&#039;;

const code = `

function greet(name: string): void {

console.log(&quot;Hello, &quot; + name);

}

`;

// Criar um arquivo virtual de origem

const sourceFile = ts.createSourceFile(

&#039;example.ts&#039;,

code,

ts.ScriptTarget.Latest,

true

);

// Função recursiva para inspecionar os nós

function visitNode(node: ts.Node, depth: number = 0) {

const indent = &#039; &#039;.repeat(depth * 2);

console.log(${indent}${ts.SyntaxKind[node.kind]});

ts.forEachChild(node, child =&gt; 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 &#039;typescript&#039;;

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: &quot;Alice&quot; };

`;

const ast = parseTypeScriptCode(&#039;models.ts&#039;, 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 &#039;typescript&#039;;

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() | | &#039;anonymous&#039;; const parameters = node.parameters.map(p =&gt; p.name?.getText() || &#039;param&#039;);

const returnType = node.type?.getText();

this.functions.push({ name, parameters, returnType });

}

ts.forEachChild(node, child =&gt; 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&lt;User&gt; {

// ...

}

`;

const sourceFile = ts.createSourceFile(&#039;math.ts&#039;, code, ts.ScriptTarget.Latest, true);

const collector = new FunctionCollector();

const functions = collector.collect(sourceFile);

console.log(JSON.stringify(functions, null, 2));

// Output:

// [

// { name: &quot;add&quot;, parameters: [&quot;a&quot;, &quot;b&quot;], returnType: &quot;number&quot; },

// { name: &quot;fetchUser&quot;, parameters: [&quot;id&quot;], returnType: &quot;Promise&lt;User&gt;&quot; }

// ]</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 &#039;typescript&#039;;

// Transformador que adiciona um prefixo a todas as variáveis

const createPrefixTransformer = (prefix: string) =&gt; {

return (context: ts.TransformationContext) =&gt; {

const visit: ts.Visitor = (node: ts.Node): ts.Node =&gt; {

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) =&gt; ts.visit(sourceFile, visit);

};

};

// Aplicar transformador

const code = `

let count: number = 0;

const message: string = &quot;hello&quot;;

var flag = true;

`;

const sourceFile = ts.createSourceFile(&#039;vars.ts&#039;, code, ts.ScriptTarget.Latest, true);

const result = ts.transform(sourceFile, [createPrefixTransformer(&#039;$&#039;)]);

const transformed = result.transformed[0];

// Verificar resultado

function printAST(node: ts.Node): string {

if (ts.isVariableDeclaration(node)) {

return node.name.getText();

}

return &#039;&#039;;

}

ts.forEachChild(transformed, child =&gt; {

ts.forEachChild(child, decl =&gt; {

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 &#039;typescript&#039;;

const createLogTransformer = () =&gt; {

return (context: ts.TransformationContext) =&gt; {

const visit: ts.Visitor = (node: ts.Node): ts.Node =&gt; {

if (ts.isFunctionDeclaration(node) &amp;&amp; node.body) {

const functionName = node.name?.getText() || &#039;anonymous&#039;;

// Criar statement de log

const logStatement = ts.factory.createExpressionStatement(

ts.factory.createCallExpression(

ts.factory.createPropertyAccessExpression(

ts.factory.createIdentifier(&#039;console&#039;),

&#039;log&#039;,

),

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) =&gt; ts.visit(sourceFile, visit);

};

};

// Uso

const code = `

function calculate(x: number): number {

return x * 2;

}

function greet(name: string): void {

console.log(&quot;Hi, &quot; + name);

}

`;

const sourceFile = ts.createSourceFile(&#039;app.ts&#039;, 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 &#039;typescript&#039;;

// Assumindo que você tem uma sourceFile transformada

const sourceFile = ts.createSourceFile(

&#039;example.ts&#039;,

&#039;const x: number = 42;&#039;,

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 &#039;typescript&#039;;

// Gerar interface automaticamente

function generateInterface(name: string, fields: Record&lt;string, string&gt;): ts.InterfaceDeclaration {

const members = Object.entries(fields).map(([fieldName, fieldType]) =&gt;

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&lt;string, string&gt;,

): ts.SourceFile {

const interfaceDecl = generateInterface(interfaceName, fields);

// Criar um arquivo virtual vazio e adicionar a interface

const sourceFile = ts.createSourceFile(

${interfaceName}.ts,

&#039;&#039;,

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(&#039;Product&#039;, {

id: &#039;number&#039;,

name: &#039;string&#039;,

price: &#039;number&#039;,

inStock: &#039;boolean&#039;,

});

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 &#039;typescript&#039;;

import * as fs from &#039;fs&#039;;

import * as path from &#039;path&#039;;

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, &#039;utf-8&#039;);

console.log(✓ Código gerado em: ${fullPath});

}

// Exemplo de uso

const generated = createSourceFileFromInterface(&#039;User&#039;, {

id: &#039;string&#039;,

email: &#039;string&#039;,

createdAt: &#039;Date&#039;,

});

saveGeneratedCode(&#039;./src/generated&#039;, &#039;User.ts&#039;, 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 &#039;typescript&#039;;

import * as fs from &#039;fs&#039;;

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&lt;string, PropertyDef[]&gt; {

const interfaces = new Map&lt;string, PropertyDef[]&gt;();

const visit = (node: ts.Node) =&gt; {

if (ts.isInterfaceDeclaration(node)) {

const interfaceName = node.name.getText();

const properties: PropertyDef[] = [];

node.members.forEach(member =&gt; {

if (ts.isPropertySignature(member)) {

const propName = member.name?.getText() | | &#039;&#039;; const propType = member.type?.getText() || &#039;any&#039;;

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 =&gt;

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 =&gt;

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 =&gt;

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) =&gt; {

const dtoClass = this.generateDTOClass(interfaceName, properties);

statements.push(dtoClass);

});

const sourceFile = ts.createSourceFile(

&#039;generated-dtos.ts&#039;,

&#039;&#039;,

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(&#039;models.ts&#039;, sourceCode);

const output = generator.print();

console.log(output);

// Salvar em arquivo

fs.writeFileSync(&#039;generated-dtos.ts&#039;, output, &#039;utf-8&#039;);</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais
Dominando NestJS Avançado: Guards, Interceptors, Pipes e Exception Filters em Projetos Reais

Introdução ao Middleware de NestJS NestJS é um framework robusto construído s...

Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis
Boas Práticas de Utility Types em TypeScript: Partial, Required, Pick, Omit e Outros para Times Ágeis

Introdução aos Utility Types Os Utility Types são uma funcionalidade poderosa...

Dominando Arrays, Tuplas e Enums em TypeScript na Prática em Projetos Reais
Dominando Arrays, Tuplas e Enums em TypeScript na Prática em Projetos Reais

Arrays em TypeScript: Fundamentos e Aplicações Práticas Arrays são um dos tip...