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.
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
và ;
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:
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
:
- Compiler Options: phần này chứa các cấu hình cho trình biên dịch Typescript.
- Input Files: phần này chứa các file TypeScript cần biên dịch.
{
"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).
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.
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:
- JavaScript Emitter: được định nghĩa trong
src/compiler/emitter.ts
, emitting source code JavaScript và source maps. - 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.
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:
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ụngskipLibCheck
để 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
Giới thiệu Kiến trúc Backend for Frontend (BFF)
Nov 16, 2024 • 10 min read
Flask là gì? Hướng dẫn tạo Ứng dụng Web với Flask
Nov 15, 2024 • 7 min read
Webhook là gì? So sánh Webhook và API
Nov 15, 2024 • 8 min read
Spring Boot là gì? Hướng dẫn Khởi tạo Project Spring Boot với Docker
Nov 14, 2024 • 6 min read
Two-Factor Authentication (2FA) là gì? Vì sao chỉ Mật khẩu thôi là chưa đủ?
Nov 13, 2024 • 7 min read
Test-Driven Development (TDD) là gì? Hướng dẫn thực hành TDD
Nov 13, 2024 • 6 min read