La TypeScript Compiler API permite acceder al AST de un programa TypeScript desde código TypeScript o JavaScript. Es la misma API que usa el compilador internamente, que usan los linters como ESLint con el plugin de TypeScript y que usan herramientas como ts-morph, prettier y ts-jest. Conocerla permite escribir linters personalizados, generadores de código, herramientas de migración automatizada y análisis estático de proyectos propios.
Crear un programa: ts.createProgram
import ts from "typescript";
import path from "path";
// Leer el tsconfig del proyecto:
const configPath = ts.findConfigFile(".", ts.sys.fileExists, "tsconfig.json");
if (!configPath) throw new Error("No se encontró tsconfig.json");
const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
const { options, fileNames } = ts.parseJsonConfigFileContent(
config, ts.sys, path.dirname(configPath)
);
// Crear el programa (análisis de tipos):
const programa = ts.createProgram(fileNames, options);
const checker = programa.getTypeChecker();
// Obtener los archivos fuente (excluyendo .d.ts de dependencias):
const archivos = programa.getSourceFiles().filter(
f => !f.fileName.includes("node_modules")
);
console.log(`Archivos analizados: ${archivos.length}`);
Recorrer el AST con ts.forEachChild
// Contar funciones en todos los archivos:
function contarFunciones(nodo: ts.Node): number {
let cuenta = 0;
if (
ts.isFunctionDeclaration(nodo) ||
ts.isFunctionExpression(nodo) ||
ts.isArrowFunction(nodo) ||
ts.isMethodDeclaration(nodo)
) {
cuenta++;
}
ts.forEachChild(nodo, hijo => {
cuenta += contarFunciones(hijo);
});
return cuenta;
}
for (const archivo of archivos) {
const n = contarFunciones(archivo);
if (n > 0) console.log(`${archivo.fileName}: ${n} funciones`);
}
Inferir tipos con TypeChecker
// Obtener el tipo inferido de cada declaración de variable:
function analizarVariables(archivo: ts.SourceFile) {
function visitar(nodo: ts.Node) {
if (ts.isVariableDeclaration(nodo) && nodo.name) {
const tipo = checker.getTypeAtLocation(nodo.name);
const tipoStr = checker.typeToString(tipo);
const posicion = archivo.getLineAndCharacterOfPosition(nodo.getStart());
console.log(
`Línea ${posicion.line + 1}: ${nodo.name.getText()} ? ${tipoStr}`
);
}
ts.forEachChild(nodo, visitar);
}
visitar(archivo);
}
// Verificar si un tipo es nullable:
function esNullable(nodo: ts.Node): boolean {
const tipo = checker.getTypeAtLocation(nodo);
const flags = tipo.getFlags();
return !!(flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined));
}
Transformaciones con ts.factory
// Transformar console.log en una función personalizada:
function transformarConsole(): ts.TransformerFactory<ts.SourceFile> {
return (contexto) => (archivoFuente) => {
function visitar(nodo: ts.Node): ts.Node {
// Detectar console.log(...)
if (
ts.isCallExpression(nodo) &&
ts.isPropertyAccessExpression(nodo.expression) &&
nodo.expression.expression.getText(archivoFuente) === "console" &&
nodo.expression.name.getText(archivoFuente) === "log"
) {
// Reemplazar por logger.info(...)
return ts.factory.createCallExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createIdentifier("logger"),
"info"
),
undefined,
nodo.arguments
);
}
return ts.visitEachChild(nodo, visitar, contexto);
}
return ts.visitEachChild(archivoFuente, visitar, contexto);
};
}
// Aplicar la transformación:
const resultado = ts.transform(archivoFuente, [transformarConsole()]);
const printer = ts.createPrinter();
const codigoTransformado = printer.printFile(resultado.transformed[0]);
ts-morph: la alternativa con más ergonomía
// ts-morph envuelve la Compiler API con una interfaz más amigable:
// npm install ts-morph
import { Project } from "ts-morph";
const proyecto = new Project({ tsConfigFilePath: "tsconfig.json" });
// Añadir una propiedad a todas las interfaces:
for (const archivo of proyecto.getSourceFiles("src/**/*.ts")) {
for (const interfaz of archivo.getInterfaces()) {
if (!interfaz.getProperty("creadoEn")) {
interfaz.addProperty({
name: "creadoEn",
type: "Date",
hasQuestionToken: true,
});
}
}
}
// Guardar todos los cambios:
await proyecto.save();
// Renombrar un símbolo en todo el proyecto:
const archivo = proyecto.getSourceFileOrThrow("src/tipos.ts");
const interfaz = archivo.getInterfaceOrThrow("Usuario");
await interfaz.rename("UsuarioDTO"); // actualiza todos los archivos que la usan
La TypeScript Compiler API tiene una curva de aprendizaje pronunciada porque expone la complejidad completa del compilador. Para la mayoría de casos de uso análisis de tipos, refactorizaciones automáticas, generación de código ts-morph es la opción práctica: usa la misma API por debajo pero con una interfaz que no requiere conocer los detalles internos del AST de TypeScript.
Imagen: Pexels / Seraphfim Gallery
