← TypeScript Thực Chiến

Bài 1 · Cơ bản · 16 phút

Vì sao TypeScript?

Biên soạn bởi Nguyễn Anh Tuấn

TypeScript là một lớp kiểu (type) phủ lên JavaScript: bắt lỗi trước khi chạy, gợi ý code, refactor an toàn. Bản chất under-the-hood: trình biên dịch tsc XOÁ sạch kiểu khi dịch ra JS (type erasure) nên kiểu không tồn tại lúc chạy - kèm khung so sánh TS gõ vào với JS xoá kiểu đi ra.

JavaScript rất dễ tính. Bạn gõ nhầm tên một thuộc tính, nó không hề báo lỗi - chỉ lặng lẽ trả về undefined, rồi cái sai trôi tiếp tới tận lúc chạy. Xem thử:

bug.js

const meo = { ten: "Bo", tuoi: 2 };
console.log("Sang nam meo " + (meo.tuoii + 1));

Kết quả khi chạy

Sang nam meo NaN

Thấy lỗi chưa? tuoii bị gõ thừa một chữ i. JavaScript đọc meo.tuoii ra undefined, rồi undefined + 1 ra NaN. Không một lời cảnh báo. Nếu đoạn này nằm sâu trong ứng dụng, lỗi có thể trôi tới tận tay người dùng mới lộ.

  • JavaScript chỉ phát hiện loại lỗi này lúc CHẠY, thường là quá muộn.
  • Gõ nhầm thuộc tính cho ra undefined một cách lặng lẽ, rồi lan thành NaN.
  • Lỗi bắt càng muộn càng đắt: muộn nhất là khi người dùng gặp.

Ai tạo ra TypeScript?

TypeScript do Microsoft phát triển, kiến trúc sư trưởng là Anders Hejlsberg - chính người trước đó tạo ra Turbo Pascal, Delphi và C#. Sau khoảng hai năm làm nội bộ, TypeScript ra mắt công khai dưới dạng mã nguồn mở vào tháng 10/2012 (bản 0.8). Mục tiêu từ đầu đã rõ: thêm kiểu tĩnh cho JavaScript để làm được dự án lớn.

TypeScript là JavaScript cộng thêm một lớp kiểu (type). Bạn nói cho công cụ biết "biến này là số, hàm này nhận chuỗi", và trình biên dịch tsc kiểm tra những lời hứa đó trước khi chạy. Vẫn đoạn code y hệt, nhưng lưu thành .ts:

bug.ts (giống hệt bug.js bên trên)

const meo = { ten: "Bo", tuoi: 2 };
console.log("Sang nam meo " + (meo.tuoii + 1));

tsc báo lỗi ngay khi gõ, chưa chạy dòng nào

error TS2551: Property 'tuoii' does not exist on type
'{ ten: string; tuoi: number; }'. Did you mean 'tuoi'?

tsc không chỉ chặn lỗi, nó còn đoán đúng ý bạn: "Did you mean 'tuoi'?". Cùng một lớp kiểu đó cho bạn hai món quà nữa khi viết code: gợi ý (autocomplete) chính xác vì editor biết hình dạng dữ liệu, và đổi tên (refactor) an toàn vì TS tìm ra mọi chỗ dùng tới.

  • tsc kiểm kiểu lúc biên dịch, bắt lỗi ngay khi gõ thay vì lúc chạy.
  • Lớp kiểu là tài liệu sống: editor gợi ý đúng và refactor không sợ sót.
  • Cùng một code, đổi đuôi .ts là có thêm một tấm lưới an toàn.

Đây là điều nhiều người dùng TypeScript cả năm vẫn chưa nắm rõ. tsc không "chạy" kiểu của bạn - nó xoá sạch kiểu rồi sinh ra JavaScript thuần. Lúc chạy, không còn TypeScript ở đâu cả, chỉ có JS. Người ta gọi đó là type erasure (xoá kiểu). Bấm thử từng ví dụ:

Bạn gõ: TypeScript (.ts)
function chao(ten: string): string {
    return "Xin chao, " + ten;
}
const loi: string = chao("Bo");
tsc sinh ra: JavaScript (.js)
function chao(ten) {
    return "Xin chao, " + ten;
}
const loi = chao("Bo");

Bị xoá: Mọi chú thích : string bay sạch; phần chạy còn lại y hệt JavaScript.

tsc còn thêm dòng "use strict"; chuẩn ở đầu file, đây lược cho gọn.

Hệ quả rất quan trọng: kiểu không làm chậm lúc chạy (vì đã biến mất), và bạn không thể hỏi một type lúc chạy ("biến này có đúng kiểu User không?") - vì tới lúc đó type đã không còn. Nhưng để ý ví dụ enum: nó là ngoại lệ, tsc sinh hẳn một object thật nên enum vẫn sống tới runtime.

Trung thực

Widget trên đơn giản hoá: dùng --target es2020 và lược dòng "use strict"; cho gọn. tsc thật có nhiều tuỳ chọn hơn, và không phải mọi thứ đều bị xoá: enum, class, namespace và parameter property đều sinh ra code JavaScript thật. Quy tắc đúng là: phần "chỉ để mô tả kiểu" thì bị xoá, phần sinh ra giá trị/hành vi thì còn.
  • tsc biên dịch TS thành JS thuần và xoá toàn bộ thông tin kiểu (type erasure).
  • Vì kiểu biến mất, không có chi phí lúc chạy và không kiểm được type ở runtime.
  • Ngoại lệ: enum/class sinh ra code thật, nên chúng còn sống sau khi biên dịch.

Đủ lý thuyết, ta cài thật. TypeScript là một gói npm; tsc là trình biên dịch. Viết một file .ts rồi dịch ra .js:

hello.ts

const ten: string = "the gioi";
console.log("Xin chao, " + ten);

Trong terminal

npm install -D typescript
npx tsc hello.ts     # sinh ra hello.js (kiểu đã bị xoá)
node hello.js

Kết quả khi chạy

Xin chao, the gioi

Muốn cấu hình một lần cho cả dự án, dùng file tsconfig.json (chạy npx tsc --init để sinh bản đầy đủ). Bản tối thiểu:

tsconfig.json tối thiểu

{
  "compilerOptions": {
    "target": "es2020",
    "module": "nodenext",
    "strict": true,
    "outDir": "dist"
  }
}

Mẹo: thử nhanh không cần cài

Muốn nghịch thử mà chưa muốn cài gì, mở TypeScript Playgroundtypescriptlang.org/play: gõ TS bên trái, thấy ngay JS sinh ra và lỗi kiểu bên phải - đúng như widget xoá kiểu ở Bước 3.

Bạn vừa nắm điều quan trọng nhất: TypeScript là một lớp kiểm tra lúc biên dịch rồi tan đi, để lại JavaScript chạy. Từ đây, cả khoá xoay quanh việc dùng lớp đó cho thật giỏi: bắt đầu từ kiểu cơ bản và suy luận kiểu, tới interface và structural typing, generics, rồi lập trình hẳn ở tầng kiểu - và khép lại bằng cách di chuyển một dự án JS sang TS.

Cảm hứng từ TypeScript Cookbook

Lộ trình khoá này lấy cảm hứng chủ đề và thứ tự từ cuốn TypeScript Cookbook (Stefan Baumgartner, O'Reilly) - cuốn đào rất sâu vào "type-level programming". Mọi ví dụ, mô phỏng và bài tập ở đây đều tự biên soạn; bạn nên tìm đọc bản gốc và xem các công thức miễn phí ở typescript-cookbook.com.

Bài tiếp theo: Kiểu cơ bản & suy luận

Ta bắt đầu xây từ nền: các kiểu cơ bản, cách TypeScript tự suy luận kiểu, và bộ ba any / unknown / never mà ai mới học cũng hay nhầm.

Câu hỏi thường gặp

Không. Vì kiểu bị xoá hết khi biên dịch (type erasure), thứ thực sự chạy là JavaScript thuần - không có lớp kiểm tra kiểu nào lúc chạy. TypeScript chỉ thêm một bước biên dịch (build) lúc bạn phát triển, không thêm gánh nặng lúc chạy.

Mặc định là không: chúng chỉ hiểu JavaScript, nên cần biên dịch .ts ra .js trước (bằng tsc). Có vài công cụ chạy .ts cho tiện khi phát triển (như tsx, ts-node hoặc Bun), nhưng bên dưới chúng vẫn lược kiểu rồi mới chạy JS.

Nên giỏi JS trước. TypeScript là JavaScript cộng thêm kiểu, không phải một ngôn ngữ khác. Khoá này giả định bạn đã qua hai khoá JavaScript, nên sẽ không dạy lại vòng lặp, hàm hay object - mà tập trung vào phần kiểu.

Vì lúc chạy, kiểu đã bị xoá sạch, không còn gì để kiểm. Bạn không viết được "nếu x là kiểu User thì..." ở runtime. Muốn kiểm dữ liệu thật lúc chạy (vd dữ liệu trả về từ API), bạn dùng kiểm tra JavaScript thường hoặc một thư viện validate - ta sẽ đụng tới ý này ở các bài sau.

Không. TypeScript biên dịch xuống JavaScript và là tập cha (superset) của JS: mọi file JavaScript hợp lệ cũng là TypeScript hợp lệ. Nó là lớp hỗ trợ lúc phát triển, còn thứ chạy cuối cùng vẫn luôn là JavaScript.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

TypeScript khác JavaScript ở điểm cốt lõi nào?

  1. 1

    Bắt con bug NaN

    Tự viết một đoạn JavaScript có lỗi gõ nhầm tên thuộc tính làm kết quả ra NaN hoặc undefined. Chạy bằng node để thấy nó KHÔNG báo lỗi gì.

    Hoàn thành khi: Một file .js chạy ra NaN/undefined vì gõ nhầm, kèm một câu giải thích vì sao JavaScript im lặng cho qua.

  2. 2

    Để TypeScript bắt giúp

    Mở TypeScript Playground (typescriptlang.org/play), dán đoạn lỗi ở bài trên vào, xem TypeScript chỉ ra lỗi gì.

    Hoàn thành khi: Chép lại thông báo lỗi TypeScript đưa ra, và chỉ rõ nó bắt đúng chỗ gõ nhầm.

  3. 3

    Cài và biên dịch

    Cài TypeScript bằng npm i -D typescript, viết một file hello.ts có chú thích kiểu, biên dịch thành hello.js rồi chạy bằng node.

    Hoàn thành khi: Có file hello.js do tsc sinh ra, và chạy node hello.js in ra đúng kết quả mong đợi.

  4. 4

    Soi type erasure

    Viết một file .ts có một interface và vài biến chú thích kiểu. Biên dịch rồi mở file .js sinh ra, liệt kê những gì đã biến mất.

    Hoàn thành khi: Danh sách cụ thể các dòng/cú pháp có trong .ts mà không còn trong .js (vd cả khối interface và các chú thích : string).

  5. 5

    Bắt ngoại lệ enum

    Viết một enum nhỏ trong .ts, biên dịch, mở file .js. Mô tả enum khác interface ở chỗ nào.

    Hoàn thành khi: Một câu kết luận: enum sinh ra code JavaScript thật (còn lúc chạy), trong khi interface biến mất hoàn toàn.

  6. 6

    Dự đoán trước khi biên dịch

    Viết một đoạn TS ngắn (5-8 dòng). TRƯỚC khi biên dịch, tự viết ra JS mà mèo con nghĩ tsc sẽ sinh. Rồi biên dịch và so.

    Hoàn thành khi: Bản dự đoán JS của mèo con đặt cạnh JS thật từ tsc, kèm ghi chú chỗ nào đoán đúng, chỗ nào sai.