Facebook Pixel

Khám Phá Cách Hoạt Động Của TypeScript Compiler

12 Aug, 2024

Tran Thuy Vy

Frontend Developer

Bạn đã code Typescript một khoảng thời gian, nhưng vẫn còn mơ hồ về cách mà Typescript biên dịch sang Javascript. Bài viết này sẽ giúp bạn hiểu rõ chi tiết về từng bước trong quá trình biên dịch.

Khám Phá Cách Hoạt Động Của TypeScript Compiler

Mục Lục

Bạn đã code Typescript một khoảng thời gian, nhưng vẫn còn mơ hồ về cách mà Typescript biên dịch sang Javascript. Bài viết này sẽ giúp bạn hiểu rõ chi tiết về từng bước trong quá trình biên dịch.

1. Typescript Compiler là gì?

TypeScript Compiler (hay tsc ) là công cụ biên dịch code TypeScript (.ts) thành JavaScript (*.js), cùng với tệp định nghĩa kiểu dữ liệu (*.d.ts) và source maps (*.js.map), giúp mã chạy trên trình duyệt và môi trường Nodejs.

tsc high level

Ngoài ra, nếu như source của bạn gặp problems, Typescript compiler sẽ giúp bạn phát hiện và khắc phục nó.

2. Các thành phần cơ bản của compiler

2.1 Parser

Parser là bộ phân tích cú pháp, chia nhỏ mã nguồn thành các phần tử (tokens) như biến, hàm và tạo ra cây biểu diễn cú pháp AST (Abstract Syntax Tree) - mỗi nút đại diện cho thành phần mã.

2.2 Binder

Binder sẽ liên kết các thành phần như biến, hàm, lớp với định nghĩa trong source code.

Ví dụ như khi bạn khai báo biến let a = 10;, compiler sẽ nhận diện a là một thành phần cụ thể là biến, và gắn kết với a là kiểu dữ liệu number.

2.3 Type Checker

Type checker là một trong những tính năng mạnh nhất của Typescript. Type checker sẽ đảm bảo tất cả các thành phần trong code đều tuân theo quy tắc mà bạn định nghĩa.

Nếu bạn khai báo một biến với kiểu là number nhưng sau đó lại cố gắng gán cho biến đó giá trị kiểu string, type checker sẽ phát hiện và báo lỗi.

2.4 Emitter

Emitter sẽ tuần tự duyệt qua các nút của AST và phát sinh ra mã Javascript tương ứng.

Ví dụ như trong typescript: let a:number = 200; sẽ được phát sinh thành var a = 200; trong javascript.

2.5 Scanner

Scanner là bộ quét mã nguồn, phân tích mã để chuyển đổi thành tokens.

Ví dụ: let a = 200; sẽ được scanner quét thành: let, a, =, 200;

3. Quá trình hoạt động compiler

Sau khi đã hiểu hơn về các thành phần cơ bản, thì mình sẽ đề cập đến quá trình hoạt động compiler:

Các thành phần cơ bản của Typescript Compiler

Quá trình biên dịch TypeScript bắt đầu khi bạn chạy lệnh tsc. Để thực hiện, TypeScript Compiler cần file tsconfig.json:

  1. Compiler Options: phần này chứa các cấu hình cho trình biên dịch Typescript.
  2. Input Files: phần này chứa các file TypeScript cần biên dịch.
Typescript
{
	"compilerOptions": {
    	...
    },
    
    "files": [
        "src/*.ts"
    ],
}

Quá trình biên dịch sẽ được tạo ra dưới dạng đối tượng Program, được định nghĩa trong file src/compiler/program.ts.

Khi đối tượng được tạo, nó sẽ tải tất cả các input files và các tệp phụ thuộc. Sau đó, gọi Parser- được định nghĩa trong tệp src/compiler/parser.ts để phân tích từng files thành AST (Abstract Syntax Tree).

tsc program parser to ast

Trong quá trình hoạt động, Parser tạo ra đối tượng Scanner được định nghĩa trong file src/compiler/scanner.ts, nó sẽ quét qua source code và sinh ra chuỗi SyntaxKind tokens.

Sau đó, AST sẽ được truyền cho Binder được định nghĩa trong tệp src/compiler/binder.ts để tạo map giữa các nút AST và Symbols.

tsc map ast binder symbols

Một Symbol bổ sung vào metadata, để lưu trữ thông tin kiểu của mỗi nút. Binder tạo ra một symbols table, sẽ được sử dụng sau này để kiểm tra kiểu dữ liệu.

Sau đó, lệnh Program.emit được gọi, Emit Worker sẽ được tạo ra để chuyển đổi AST thành code JavaScript và các tệp khác. Có hai loại emitter:

  1. JavaScript Emitter: được định nghĩa trong src/compiler/emitter.ts, emitting source code JavaScript và source maps.
  2. Type Definition Emitter: được định nghĩa trong src/compiler/definitionEmitter.ts, emitting các file định nghĩa kiểu dữ liệu.

Khi Emitter chạy, nó sẽ gọi hàm getDiagnostics() để tạo một Type Checker, được định nghĩa trong file src/compiler/checker.ts. Sau đó, Emitter sẽ duyệt qua AST để xử lý từng nút.

Trên mỗi nút, nó sẽ phân tích mã, sử dụng dữ liệu kiểu từ Symbols Table, và nếu mọi thứ không có vấn đề, JavaScript sources cuối cùng sẽ được sinh ra.

tsc emitter

4. Báo lỗi khi Typescript Compiler

Nhiều loại lỗi khác nhau có thể được trả về trong quá trình biên dịch, phụ thuộc vào giai đoạn trình biên dịch tìm thấy lỗi.

Đây là cách các lỗi được định nghĩa và sử dụng trong hệ thống compiles:

Typescript
enum BuildResultFlags {
    None = 0,
    Success = 1 << 0,
    DeclarationOutputUnchanged = 1 << 1,
 
    ConfigFileErrors = 1 << 2,
    SyntaxErrors = 1 << 3,
    TypeErrors = 1 << 4,
    DeclarationEmitErrors = 1 << 5,
    EmitErrors = 1 << 6,
 
    AnyErrors = ConfigFileErrors | SyntaxErrors | TypeErrors | DeclarationEmitErrors | EmitErrors
}
  • Nếu có lỗi trong tệp tsconfig.json, ConfigFileErrors sẽ được trả về.
  • Nếu lỗi được Scanner tìm thấy,  SyntaxErrors được trả về.
  • Nếu code được viết đúng cú pháp, nhưng không đúng ngữ nghĩa, đa phần sẽ trả về TypeErrors.

5. Tối ưu hóa quá trình biên dịch

  • Sử dụng tsc --watch: cho phép compiler giám sát thay đổi trong source code, tự động compiles chỉ thành phần thay đổi.
  • Tối ưu cấu trúc dự án: chia dự án thành các module nhỏ hơn dễ bảo trì source code.
  • Sử dụng skipLibCheck: Khi làm việc với các thư viện bên ngoài, bạn có thể sử dụng skipLibCheck để bỏ qua kiểm tra kiểu của các file .d.ts .

6. Kết luận

Hy vọng thông qua bài viết này, bạn đã hiểu rõ quá trình chi tiết compiles.

Nếu bạn quan tâm các chủ đề khác liên quan, bạn có thể tham khảo một số bài viết dưới đây:

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab