Menu

Best practice TypeScript: 8 quy tắc kèm ví dụ trước và sau

Tám thói quen TypeScript giúp ngăn bug thật: luôn bật strict, dùng unknown thay cho any, để suy luận kiểu làm việc, ưu tiên union hơn enum, kiểm tra config bằng satisfies, mô hình hóa state bằng discriminated union, tránh ! và as, và đặt dữ liệu là readonly. Mỗi quy tắc đi kèm ví dụ trước và sau chạy được.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Các quy tắc dưới đây là những quy tắc ngăn được nhiều bug nhất trong code TypeScript thực tế. Mỗi quy tắc cho thấy cách viết thường gặp trước, rồi đến cách tốt hơn, bằng code bạn chạy được. Quy tắc đầu tiên quan trọng nhất: ngừng dùng any.

Dùng unknown thay cho any

any tắt kiểm tra kiểu cho một giá trị và mọi thứ được tính từ nó. unknown cũng nhận mọi giá trị, nhưng bạn phải kiểm tra trước khi dùng, nhờ đó phép kiểm tra nằm đúng ở chỗ dữ liệu đi vào chương trình:

Ranh giới là những nơi kiểu không còn được bảo đảm: JSON.parse, response của fetch, localStorage, dữ liệu nhập từ form, biến môi trường và thông điệp từ tiến trình khác. Hãy kiểm tra ở đó bằng type guard hoặc thư viện schema, và phần còn lại của code có thể tin tưởng kiểu của mình.

Luôn bật strict

strict là mặc định trong TypeScript 7; đừng tắt nó. Hãy thêm những phép kiểm tra mà nó bỏ qua nhưng bắt được nhiều bug nhất:

{
    "compilerOptions": {
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "noImplicitOverride": true,
        "noFallthroughCasesInSwitch": true
    }
}

noUncheckedIndexedAccess khiến arr[i] và record[key] bao gồm cả undefined, đúng với giá trị chúng trả về khi chỉ số không tồn tại. noImplicitOverride buộc phương thức của lớp con ghi rõ override, còn noFallthroughCasesInSwitch từ chối một case chạy lọt sang case tiếp theo.

Để suy luận kiểu làm việc

Chú thích kiểu cho những gì TypeScript không thể biết: tham số của hàm, và kiểu trả về của các hàm mà module khác dùng. Để biến cục bộ và tham số của callback cho suy luận kiểu. Một chú thích không cần thiết không chỉ gây rối mắt; nó còn có thể khiến kiểu rộng hơn giá trị:

Nếu không có chú thích @ts-expect-error, setStatus(annotated) là lỗi TS2345. const được suy luận giữ kiểu literal "active", nên được chấp nhận. Di chuột lên biến trong trình soạn thảo để xem kiểu đã được suy luận trước khi bạn thêm kiểu.

Ưu tiên union type hơn enum

Một union các chuỗi literal cho khả năng tự động gợi ý và kiểm tra đầy đủ mà không sinh ra code. Khi bạn còn cần danh sách giá trị lúc runtime, hãy suy ra kiểu từ một mảng as const:

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

function canEdit(role: Role): boolean {
  return role !== "viewer";
}

console.log(canEdit("editor"));     // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]

canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.

Một enum Role { Admin, Viewer } biên dịch thành một đối tượng có ánh xạ ngược, và tham số kiểu enum số nhận bất kỳ biến number nào, kể cả biến chứa giá trị mà enum không liệt kê. Enum cũng không chạy được với type stripping của Node (TypeScript enum is not supported in strip-only mode). Các đánh đổi được so sánh ở trang enum.

Kiểm tra đối tượng config bằng satisfies

Chú thích một đối tượng bằng kiểu rộng như Record<string, Route> sẽ kiểm tra giá trị nhưng quên mất các khóa. satisfies kiểm tra cùng điều đó mà vẫn giữ kiểu chính xác:

Dùng chú thích kiểu khi biến cần có đúng kiểu đã khai báo (tham số của hàm, giá trị bạn sẽ gán lại). Dùng satisfies cho bảng tra cứu, bản đồ route, theme token và các đối tượng hằng khác.

Mô hình hóa state bằng discriminated union

Một đối tượng duy nhất với các field tùy chọn cho phép những trạng thái không thể xảy ra: loading: true cùng lúc với error, hoặc thiếu data sau khi thành công. Một union các đối tượng có chung một thẻ phân biệt chỉ cho phép những trạng thái thật, và mỗi nhánh chỉ thấy field của riêng nó:

Dòng never là phép kiểm tra đầy đủ. Khi ai đó thêm một trạng thái mới mà quên xử lý, quá trình build sẽ hỏng tại dòng đó:

Lỗi là index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Nó chỉ rõ trường hợp chưa được xử lý. Thêm case "cancelled": và code sẽ biên dịch được.

Tránh ! và as

Non-null assertion x! và type assertion x as T bảo trình biên dịch ngừng kiểm tra. Cả hai đều không thay đổi giá trị lúc runtime, nên một assertion sai sẽ thành lỗi crash về sau, ở xa nguyên nhân gốc:

Thay ! bằng ?., ??, một return sớm, hoặc ném lỗi kèm thông báo hữu ích. Thay as bằng một type guard thực sự kiểm tra giá trị. Assertion duy nhất luôn an toàn là as const, vì nó chỉ làm kiểu hẹp hơn và chỉ đọc. Một thiết lập lint tốt (no-non-null-assertion và no-explicit-any của typescript-eslint) sẽ đánh dấu phần còn lại.

Đặt dữ liệu là readonly

Đánh dấu thuộc tính và mảng là readonly khi code không được thay đổi chúng. Khi đó trình biên dịch từ chối push, sort và các phép gán, và các hàm trả về giá trị mới thay vì sửa đổi đầu vào:

readonly chỉ được kiểm tra lúc biên dịch và chỉ ở một cấp: nó không đóng băng đối tượng lúc runtime. Như vậy vẫn đủ để bắt những lần vô tình sửa đổi state dùng chung, nguyên nhân của phần lớn các bug kiểu này.

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

Có nên dùng any trong TypeScript không?

Gần như không bao giờ trong code ứng dụng. any tắt kiểm tra cho giá trị đó và mọi thứ tính từ nó. Dùng unknown cho những giá trị chưa biết kiểu, rồi thu hẹp chúng bằng các phép kiểm tra; chỉ giữ any cho những lối thoát hiếm hoi, kèm chú thích giải thích lý do.

Có nên chú thích kiểu cho mọi biến trong TypeScript không?

Không. Hãy để TypeScript suy luận biến cục bộ và tham số của callback. Chú thích kiểu cho tham số của hàm (chúng không thể được suy luận) và kiểu trả về của các hàm được export, để một thay đổi bên trong hàm không âm thầm làm đổi kiểu công khai của nó.

Dùng enum trong TypeScript có phải là thói quen xấu không?

Không sai, nhưng nhiều nhóm tránh dùng. Enum sinh ra code lúc runtime, không chạy được với type stripping của Node, và tham số kiểu enum số nhận bất kỳ biến number nào, dù nó chứa giá trị gì. Một union các chuỗi literal, hoặc một mảng as const cùng kiểu suy ra từ nó, cho cùng khả năng tự động gợi ý và kiểm tra mà không sinh ra code.

Khi nào nên dùng type assertion với as?

Chỉ khi bạn biết điều mà trình biên dịch không thể biết, và tốt nhất là ngay sau một phép kiểm tra chứng minh điều đó. as không thay đổi giá trị nào lúc runtime, nên {} as User vẫn biên dịch được rồi sau đó lại không có name. Trong hầu hết trường hợp, một type guard kiểm tra giá trị là công cụ an toàn hơn.

Thiết lập tsconfig nào tốt nhất cho dự án TypeScript mới?

Giữ strict bật (mặc định của TypeScript 7) và thêm noUncheckedIndexedAccess. Nhiều dự án còn bật noImplicitOverride, noFallthroughCasesInSwitch và verbatimModuleSyntax. Tệp config mà tsc --init tạo ra đặt strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes và verbatimModuleSyntax, cùng một số tùy chọn khác.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu