Eksploracja wnętrza kompilatora TypeScript

Kompilator TypeScript, często nazywany tsc, jest jednym z podstawowych komponentów ekosystemu TypeScript. Transformuje kod TypeScript na JavaScript, wymuszając jednocześnie statyczne reguły typowania. W tym artykule zagłębimy się w wewnętrzne działanie kompilatora TypeScript, aby lepiej zrozumieć, jak przetwarza i transformuje kod TypeScript.

1. Proces kompilacji TypeScript

Kompilator TypeScript wykonuje szereg kroków, aby przekształcić TypeScript w JavaScript. Oto ogólny przegląd procesu:

  1. Parsowanie plików źródłowych do drzewa składni abstrakcyjnej (AST).
  2. Wiązanie i sprawdzanie typów AST.
  3. Wysyłanie kodu wyjściowego JavaScript i deklaracji.

Przyjrzyjmy się tym krokom bardziej szczegółowo.

2. Analizowanie kodu TypeScript

Pierwszym krokiem w procesie kompilacji jest parsowanie kodu TypeScript. Kompilator pobiera pliki źródłowe, parsuje je do AST i wykonuje analizę leksykalną.

Oto uproszczony widok tego, jak można uzyskać dostęp do pliku AST i nim manipulować za pomocą wewnętrznego interfejsu API języka TypeScript:

import * as ts from 'typescript';

const sourceCode = 'let x: number = 10;';
const sourceFile = ts.createSourceFile('example.ts', sourceCode, ts.ScriptTarget.Latest);

console.log(sourceFile);

Funkcja createSourceFile służy do konwersji surowego kodu TypeScript na AST. Obiekt sourceFile zawiera przeanalizowaną strukturę kodu.

3. Wiązanie i sprawdzanie typu

Po parsowaniu następnym krokiem jest powiązanie symboli w AST i przeprowadzenie kontroli typu. Ta faza zapewnia, że ​​wszystkie identyfikatory są powiązane z odpowiednimi deklaracjami i sprawdza, czy kod przestrzega reguł typu TypeScript.

Sprawdzanie typu jest wykonywane przy użyciu klasy TypeChecker. Oto przykład, jak utworzyć program i pobrać informacje o typie:

const program = ts.createProgram(['example.ts'], {});
const checker = program.getTypeChecker();

// Get type information for a specific node in the AST
sourceFile.forEachChild(node => {
    if (ts.isVariableStatement(node)) {
        const type = checker.getTypeAtLocation(node.declarationList.declarations[0]);
        console.log(checker.typeToString(type));
    }
});

W tym przykładzie TypeChecker sprawdza typ deklaracji zmiennej i pobiera informacje o typie z AST.

4. Emisja kodu

Po zakończeniu sprawdzania typów kompilator przechodzi do fazy emisji. To tutaj kod TypeScript jest przekształcany w JavaScript. Wyjście może również zawierać pliki deklaracji i mapy źródłowe, w zależności od konfiguracji.

Oto prosty przykład użycia kompilatora do wyemitowania kodu JavaScript:

const { emitSkipped, diagnostics } = program.emit();

if (emitSkipped) {
    console.error('Emission failed:');
    diagnostics.forEach(diagnostic => {
        const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
        console.error(message);
    });
} else {
    console.log('Emission successful.');
}

Funkcja program.emit generuje dane wyjściowe JavaScript. Jeśli podczas emisji wystąpią jakieś błędy, zostaną one przechwycone i wyświetlone.

5. Wiadomości diagnostyczne

Jednym z kluczowych obowiązków kompilatora TypeScript jest dostarczanie programistom znaczących komunikatów diagnostycznych. Komunikaty te są generowane zarówno podczas fazy sprawdzania typu, jak i emisji kodu. Diagnostyka może obejmować ostrzeżenia i błędy, pomagając programistom szybko identyfikować i rozwiązywać problemy.

Oto jak pobrać i wyświetlić diagnostykę z kompilatora:

const diagnostics = ts.getPreEmitDiagnostics(program);

diagnostics.forEach(diagnostic => {
    const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
    console.log(`Error ${diagnostic.code}: ${message}`);
});

W tym przykładzie diagnostyka jest wyodrębniana z programu i drukowana na konsoli.

6. Transformacja języka TypeScript za pomocą interfejsów API kompilatora

API kompilatora TypeScript pozwala programistom tworzyć niestandardowe transformacje. Możesz modyfikować AST przed emisją kodu, umożliwiając zaawansowane dostosowania i narzędzia do generowania kodu.

Oto przykład prostej transformacji, która zmienia nazwy wszystkich zmiennych na newVar:

const transformer = (context: ts.TransformationContext) => {
    return (rootNode: T) => {
        function visit(node: ts.Node): ts.Node {
            if (ts.isVariableDeclaration(node)) {
                return ts.factory.updateVariableDeclaration(
                    node,
                    ts.factory.createIdentifier('newVar'),
                    node.type,
                    node.initializer
                );
            }
            return ts.visitEachChild(node, visit, context);
        }
        return ts.visitNode(rootNode, visit);
    };
};

const result = ts.transform(sourceFile, [transformer]);
console.log(result.transformed[0]);

Ta transformacja odwiedza każdy węzeł w AST i zmienia nazwy zmiennych w razie potrzeby.

Wniosek

Eksploracja wnętrza kompilatora TypeScript zapewnia głębsze zrozumienie tego, jak kod TypeScript jest przetwarzany i przekształcany. Niezależnie od tego, czy chcesz tworzyć niestandardowe narzędzia, czy też poprawić swoją wiedzę na temat działania TypeScript, zagłębianie się w wnętrze kompilatora może być oświecającym doświadczeniem.