← TypeScript Thực Chiến

Bài 11 · Vận dụng · 22 phút

Di chuyển JS sang TS

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

Chuyển dần một dự án JavaScript sang TypeScript: bật allowJs / checkJs, chú thích kiểu bằng JSDoc, siết strict từng bước và xử lý any tạm thời; chiến lược refactor tránh viết lại từ đầu. Đưa TypeScript vào một dự án thật.

Hầu hết dự án đến với TypeScript khi đã là một codebase JavaScript đang chạy. Cám dỗ lớn nhất là "viết lại hết cho sạch" - và đó thường là sai lầm đắt giá. Cách bền vững là di chuyển dần: cho TypeScript và JavaScript sống chung rồi thêm kiểu từng phần. Tuỳ chọn allowJs trong tsconfig cho phép điều đó:

tsconfig.json cho giai đoạn di chuyển

{
  "compilerOptions": {
    "allowJs": true,    // cho phep co ca file .js
    "checkJs": true,    // va soi kieu luon ca file .js
    "outDir": "dist"
  }
}
  • Di chuyển dần an toàn hơn viết lại: vừa chạy vừa thêm kiểu, không chặn tính năng mới.
  • allowJs cho .js và .ts cùng tồn tại trong một dự án.
  • checkJs bật kiểm kiểu cho cả file .js, không cần đổi đuôi ngay.

Bất ngờ thú vị: bạn gắn kiểu cho file .js mà KHÔNG cần đổi đuôi, qua JSDoc. Thêm // @ts-check ở đầu file là TypeScript bắt đầu soi, và JSDoc cho nó biết kiểu:

math.js (vẫn là .js!)

// @ts-check
/**
 * @param {number} a
 * @param {number} b
 * @returns {number}
 */
function tong(a, b) {
  return a + b;
}

tong("2", 3);   // "2" la string, khong phai number

tsc báo (dù là file .js)

error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
  • // @ts-check bật kiểm kiểu cho riêng một file .js.
  • JSDoc @param/@returns/@type gắn kiểu mà không đổi đuôi file.
  • Đây là cây cầu đầu tiên: có an toàn kiểu ngay trên code JS cũ.

Khi một file đã ổn với JSDoc, đổi nó sang .ts và chuyển chú thích vào thẳng code. Sẽ có chỗ khó hoặc phụ thuộc thứ chưa migrate - lúc đó dùng any hoặc @ts-expect-error như lối thoát tạm thời, kèm ghi chú để còn quay lại:

loi-thoat-tam.ts

// @ts-expect-error - TODO: sua kieu sau khi migrate xong module cu
const cauHinh: number = layCauHinhCu();

// Hoac danh dau ro bang any tam thoi
/** @type {any} TODO: thay bang kieu that */
let duLieu = layDuLieuChuaCoKieu();

@ts-expect-error tự dọn rác

Ưu tiên @ts-expect-error hơn @ts-ignore: nếu dòng kế tiếp KHÔNG còn lỗi (vì bạn đã sửa), @ts-expect-error sẽ tự báo lỗi nhắc bạn xoá nó đi. Nhờ vậy các "món nợ" tạm thời không bị quên mãi.

Đừng bật strict toàn cục ngay từ đầu trên một dự án JS lớn - hàng nghìn lỗi đổ ra một lúc làm nản lòng. Đi từng bước:

  • Bắt đầu lỏng (strict tắt, hoặc bật từng cờ một như noImplicitAny trước).
  • Di chuyển từ module "lá" (ít phụ thuộc) vào dần các file phụ thuộc.
  • Khi phần lớn đã có kiểu, bật strict toàn cục và dọn nốt các any/@ts-expect-error còn nợ.

Trung thực

Di chuyển một dự án lớn là việc nhiều tuần, không phải một buổi. Mục tiêu mỗi bước là luôn xanh (biên dịch và chạy được), tiến thêm một chút an toàn kiểu, rồi commit. Tiến từng bước nhỏ kiểm được luôn thắng một cú "đại tu" rủi ro.

Di chuyển sang TypeScript là một hành trình dần: cho JS và TS sống chung bằng allowJs, gắn kiểu bằng JSDoc, đổi đuôi rồi siết strict từng bước, dùng lối thoát tạm có đánh dấu. Mèo con giờ có đủ công cụ để đưa TypeScript vào bất kỳ dự án thật nào.

Bài cuối: Dự án cuối khoá

Khép lại bằng dự án cuối khoá: ba dự án nhỏ chạy được, ghép mọi thứ đã học - một thư viện generic an toàn kiểu, một helper type tự dựng, và di chuyển một module JavaScript sang TypeScript strict.

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

Hầu như không. Viết lại tốn kém, dễ tái tạo bug cũ, và dừng được mọi tính năng mới trong lúc làm. Di chuyển dần - vừa chạy vừa thêm kiểu, file nào xong file đó - an toàn hơn nhiều và không chặn công việc.

JSDoc rất tốt cho giai đoạn ĐẦU: bạn gắn kiểu cho .js mà chưa phải đổi đuôi hay đổi quy trình build. Nhưng về lâu dài, viết kiểu thẳng trong .ts gọn và mạnh hơn nhiều (generics, union... viết bằng JSDoc khá dài dòng). JSDoc là cây cầu, không phải đích đến.

Không sao nếu là TẠM và có đánh dấu. Trong lúc migrate, một any kèm ghi chú TODO giúp bạn đi tiếp thay vì kẹt. Vấn đề chỉ đến khi any nằm im mãi mãi và trở thành lỗ thủng vĩnh viễn - nên ghi rõ để còn quay lại dọn.

Cả hai chặn một lỗi trên dòng kế tiếp. Khác biệt: @ts-expect-error sẽ BÁO LỖI nếu dòng đó hoá ra KHÔNG có lỗi - tức nó tự nhắc bạn xoá khi đã sửa xong. @ts-ignore thì im lặng kể cả khi không còn lỗi. Ưu tiên @ts-expect-error vì nó tự dọn rác.

Từ cấu hình (allowJs, checkJs) rồi tới các module "lá" - những file ít phụ thuộc thứ khác. Gắn kiểu chúng trước, rồi đi dần lên các file phụ thuộc. Cuối cùng mới siết strict toàn cục. Đi từ ngoài rìa vào trong, từng bước nhỏ kiểm được.

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

Cách di chuyển một dự án JavaScript lớn sang TypeScript nên là gì?

Bài tập về nhà

  1. 1

    Bật cho JS và TS sống chung

    Trong một dự án JS nhỏ, thêm tsconfig với allowJs và checkJs. Quan sát tsc bắt đầu soi cả file .js.

    ✅ Hoàn thành khi: Cấu hình allowJs + checkJs hoạt động; tsc báo ít nhất một lỗi tiềm ẩn trong code .js cũ.

  2. 2

    Gắn kiểu bằng JSDoc

    Thêm // @ts-check vào một file .js, rồi dùng JSDoc (@param, @returns) cho một hàm. Gọi hàm với kiểu sai xem tsc bắt.

    ✅ Hoàn thành khi: Hàm có JSDoc; gọi sai kiểu bị báo TS2345, dù file vẫn là .js.

  3. 3

    Đổi đuôi .js sang .ts

    Đổi một file .js đã có JSDoc thành .ts. Chuyển chú thích JSDoc thành chú thích kiểu TypeScript thường.

    ✅ Hoàn thành khi: File .ts biên dịch sạch với cùng kiểu, không còn cần JSDoc cho phần đó.

  4. 4

    Lối thoát tạm thời

    Tìm một chỗ migrate khó. Dùng @ts-expect-error kèm ghi chú TODO để đi tiếp, rồi liệt kê các TODO đó.

    ✅ Hoàn thành khi: Một danh sách các @ts-expect-error còn nợ kèm lý do, để biết còn gì cần dọn.

  5. 5

    Kế hoạch siết strict

    Viết một kế hoạch 3-4 bước siết strict cho dự án: bắt đầu từ đâu, bật cờ nào trước.

    ✅ Hoàn thành khi: Một lộ trình có thứ tự (vd: allowJs/checkJs → JSDoc các module lá → đổi sang .ts → bật strict), kèm lý do thứ tự.

  6. 6

    Di chuyển một module lá

    Chọn một module nhỏ ít phụ thuộc trong dự án của mèo con. Di chuyển nó sang TypeScript có kiểu chặt, đầu cuối.

    ✅ Hoàn thành khi: Module đó là .ts biên dịch sạch dưới strict, các chỗ dùng nó vẫn chạy như cũ.