컴파일러 API — ts.createProgram으로 AST 분석과 코드 생성
TypeScript Compiler API는 TypeScript 코드를 프로그래밍적으로 파싱, 분석, 변환, 생성 할 수 있는 공식 API입니다. 커스텀 린트 규칙이나 코드 생성기를 만들 때 사용합니다.
기본 개념
TypeScript 컴파일러는 다음 단계를 거칩니다:
소스 코드 → Scanner → Token → Parser → AST → Binder → Type Checker → Emitter → JS 코드
Compiler API로 이 파이프라인의 각 단계에 접근할 수 있습니다.
프로그램 생성
import * as ts from 'typescript';
// tsconfig.json을 읽어서 프로그램 생성
const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
const configFile = ts.readConfigFile(configPath!, ts.sys.readFile);
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
'./'
);
const program = ts.createProgram({
rootNames: parsedConfig.fileNames,
options: parsedConfig.options,
});
// 타입 체커 가져오기
const checker = program.getTypeChecker();
AST 탐색
// 소스 파일의 AST를 순회
function visit(node: ts.Node) {
// 함수 선언 찾기
if (ts.isFunctionDeclaration(node) && node.name) {
console.log(`함수 발견: ${node.name.text}`);
// 반환 타입 확인
const signature = checker.getSignatureFromDeclaration(node);
if (signature) {
const returnType = checker.getReturnTypeOfSignature(signature);
console.log(` 반환 타입: ${checker.typeToString(returnType)}`);
}
}
// 인터페이스 선언 찾기
if (ts.isInterfaceDeclaration(node)) {
console.log(`인터페이스 발견: ${node.name.text}`);
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.name) {
const name = (member.name as ts.Identifier).text;
const type = member.type ? node.getSourceFile().text.slice(
member.type.pos, member.type.end
).trim() : 'unknown';
console.log(` 속성: ${name}: ${type}`);
}
});
}
// 자식 노드 재귀 탐색
ts.forEachChild(node, visit);
}
// 모든 소스 파일 탐색
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
visit(sourceFile);
}
}
타입 정보 추출
// 특정 노드의 타입 정보 가져오기
function getTypeInfo(node: ts.Node, checker: ts.TypeChecker) {
const type = checker.getTypeAtLocation(node);
return {
typeName: checker.typeToString(type),
properties: type.getProperties().map((prop) => ({
name: prop.name,
type: checker.typeToString(
checker.getTypeOfSymbolAtLocation(prop, node)
),
})),
};
}
코드 생성
// AST 노드를 프로그래밍적으로 생성
const factory = ts.factory;
// interface User { name: string; age: number; } 생성
const interfaceDecl = factory.createInterfaceDeclaration(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
'User',
undefined,
undefined,
[
factory.createPropertySignature(
undefined,
'name',
undefined,
factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
),
factory.createPropertySignature(
undefined,
'age',
undefined,
factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)
),
]
);
// AST를 문자열로 변환
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const sourceFile = ts.createSourceFile('output.ts', '', ts.ScriptTarget.Latest);
const result = printer.printNode(ts.EmitHint.Unspecified, interfaceDecl, sourceFile);
console.log(result);
// export interface User {
// name: string;
// age: number;
// }
실전 활용: 커스텀 코드 생성기
// 데이터베이스 스키마에서 TypeScript 타입 자동 생성
interface Column {
name: string;
type: 'string' | 'number' | 'boolean' | 'date';
nullable: boolean;
}
interface Table {
name: string;
columns: Column[];
}
function generateInterface(table: Table): ts.InterfaceDeclaration {
const members = table.columns.map((col) => {
let typeNode: ts.TypeNode;
switch (col.type) {
case 'string': typeNode = factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); break;
case 'number': typeNode = factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); break;
case 'boolean': typeNode = factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); break;
case 'date': typeNode = factory.createTypeReferenceNode('Date'); break;
}
if (col.nullable) {
typeNode = factory.createUnionTypeNode([
typeNode!,
factory.createLiteralTypeNode(factory.createNull()),
]);
}
return factory.createPropertySignature(
undefined,
col.name,
col.nullable ? factory.createToken(ts.SyntaxKind.QuestionToken) : undefined,
typeNode!
);
});
return factory.createInterfaceDeclaration(
[factory.createModifier(ts.SyntaxKind.ExportKeyword)],
table.name,
undefined,
undefined,
members
);
}
진단(Diagnostics) 추출
// 컴파일 에러를 프로그래밍적으로 가져오기
const diagnostics = ts.getPreEmitDiagnostics(program);
diagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const { line, character } = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start!
);
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
}
});
정리
- TypeScript Compiler API로 코드를 파싱, 분석, 생성할 수 있다
ts.createProgram으로 프로그램을 만들고getTypeChecker로 타입 정보에 접근한다- AST를 순회하면서 함수, 인터페이스, 변수 등의 정보를 추출할 수 있다
ts.factory로 새로운 AST 노드를 생성하고ts.createPrinter로 문자열로 변환한다- ORM 타입 생성기, 커스텀 린트 규칙, API 클라이언트 생성기 등에 활용된다
댓글 로딩 중...