TypeScript Compiler API는 TypeScript 코드를 프로그래밍적으로 파싱, 분석, 변환, 생성 할 수 있는 공식 API입니다. 커스텀 린트 규칙이나 코드 생성기를 만들 때 사용합니다.

기본 개념

TypeScript 컴파일러는 다음 단계를 거칩니다:

PLAINTEXT
소스 코드 → Scanner → Token → Parser → AST → Binder → Type Checker → Emitter → JS 코드

Compiler API로 이 파이프라인의 각 단계에 접근할 수 있습니다.

프로그램 생성

TYPESCRIPT
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 탐색

TYPESCRIPT
// 소스 파일의 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);
  }
}

타입 정보 추출

TYPESCRIPT
// 특정 노드의 타입 정보 가져오기
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)
      ),
    })),
  };
}

코드 생성

TYPESCRIPT
// 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
// 데이터베이스 스키마에서 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) 추출

TYPESCRIPT
// 컴파일 에러를 프로그래밍적으로 가져오기
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 클라이언트 생성기 등에 활용된다
댓글 로딩 중...