· typescript / strictness / bugs

TypeScript strict mode thực sự giảm tỷ lệ bug không?

TypeScript strict mode phát hiện ~15% bug đã commit — không phải 38% Airbnb tuyên bố. Kết luận từ nghiên cứu độc lập (n=400) và 3 case study thực tế.

Bởi

2.842 từ · 15 phút đọc

Con số trung thực nhất từ các nghiên cứu độc lập: TypeScript strict mode phát hiện 15% bug JavaScript đã được commit công khai. Không phải 38% như bài nói chuyện tại hội nghị của Airbnb khẳng định. Cũng không phải 0% như nhóm hoài nghi lập luận. Con số thực tế nằm đâu đó giữa hai đầu, với ngưỡng trần phụ thuộc vào việc bạn có bật thêm noUncheckedIndexedAccess hay không — một flag mà TypeScript team cố tình loại khỏi strict.

Hãy bật strict: true. Chi phí migration có thật và tập trung ở giai đoạn đầu, nhưng TypeScript 6.0 đã đặt nó làm mặc định vào năm 2026. Đặt kỳ vọng chính xác, đừng thổi phồng.

Bài này dành cho ai

Bạn đang cân nhắc bật strict: true trên một codebase TypeScript đã có sẵn, đánh giá xem nỗi đau migration có đáng không, hoặc đang cố trả lời câu hỏi “cái này có thực sự giảm bug không?” bằng dữ liệu thay vì cảm giác. Đây không phải hướng dẫn cách bật strict mode — đây là đánh giá bằng chứng thực nghiệm.

Nếu codebase của bạn đã dùng strict, phần kết luận có danh sách những gì bạn vẫn còn thiếu.

strict: true thực sự bật những gì

strict: true trong tsconfig.json là cách viết tắt cho chín compiler flag. Tính đến TypeScript 5.x:

FlagTác dụng
strictNullChecksnull/undefined không được gán vào kiểu non-null; buộc phải xử lý tường minh
noImplicitAnyBiến/tham số không có kiểu có thể suy ra phải được annotation
strictFunctionTypesKiểu tham số hàm được kiểm tra contravariant khi gán
strictPropertyInitializationCác field của class phải được khởi tạo trong constructor hoặc đánh dấu !
strictBindCallApply.call(), .bind(), .apply() được kiểm tra theo chữ ký gốc
noImplicitThisthis trong hàm phải có kiểu được khai báo
useUnknownInCatchVariablescatch (e) trả về e: unknown thay vì e: any (TS 4.4+)
alwaysStrictThêm "use strict" vào output và phân tích file ở strict ECMAScript mode
strictBuiltinIteratorReturnIterator tích hợp sẵn được typed với TReturn = undefined (TS 5.6+)

strictNullChecksnoImplicitAny đảm nhận phần lớn công việc ngăn bug. Phần còn lại thực sự hữu ích nhưng ít tác động trong thực tế hơn.

Những gì cố tình bị loại khỏi strict

Ba flag mang lại lợi ích an toàn đáng kể nhưng bị loại khỏi strict:

noUncheckedIndexedAccess là thiếu sót quan trọng nhất. Truy cập mảng arr[i] và object obj[key] trả về T | undefined thay vì T. Flag này bắt nguồn gốc phổ biến nhất của lỗi Cannot read properties of undefined trong TypeScript. TypeScript team đã từ chối đưa nó vào (GitHub issue #49169, đóng với lý do “Declined — doesn’t match the TypeScript vision”) vì trên các codebase hiện có, nó tạo ra hàng trăm lỗi và khiến việc migration trở nên quá tốn kém.

exactOptionalPropertyTypes ngăn gán undefined vào thuộc tính tùy chọn foo?: string; bạn phải khai báo tường minh là foo?: string | undefined. Bịt một lỗ hổng tinh vi khi { foo: undefined }{} được xem là tương đương.

noUncheckedSideEffectImports (TS 6.0+) bắt các side-effect import bị gõ sai.

TypeScript không cố gắng đúng hoàn toàn

Design Goals của TypeScript liệt kê điều này như một mục tiêu không theo đuổi:

“Apply a sound or ‘provably correct’ type system. Instead, strike a balance between correctness and productivity.”

TypeScript cố tình không hoàn toàn chính xác theo nhiều cách: structural typing, type assertions, any. Điều này đặt ra giới hạn cho những gì strict mode có thể đảm bảo.

TypeScript 6.0 (2026) đặt strict: true làm mặc định mới cho tsconfig và deprecated alwaysStrict: false. Tín hiệu rõ ràng: strict không còn là tính năng an toàn tùy chọn nữa, mà là yêu cầu cơ bản.

Dữ liệu nói gì

Con số từ nghiên cứu độc lập: 15%

Gao, Bird, và Barr tại ICSE 2017 (To Type or Not to Type: Quantifying Detectable Bugs in JavaScript) lấy mẫu 400 bug đã được xác nhận từ 398 dự án JavaScript công khai trên GitHub. Họ thủ công thêm type annotation vào code trước khi sửa và kiểm tra xem TypeScript 2.0 có báo lỗi hay không.

Kết quả: TypeScript phát hiện 58 trong 400 bug — 15% (95% CI: 11.5%–18.5%).

Giới hạn mà tác giả thừa nhận: chỉ nghiên cứu các bug đã được báo cáo và sửa công khai (không bắt được bug nội bộ); giới hạn 10 phút annotation cho mỗi bug; nghiên cứu dùng TypeScript 2.0 với cài đặt mặc định, không phải strict: true. Con số 15% phản ánh nỗ lực annotation một phần trên mẫu hạn chế — hãy xem đây là ngưỡng sàn, không phải ngưỡng trần, cho một codebase có đầy đủ annotation với tất cả strict flag.

Flow 0.30 phát hiện gần tương đương (59/400), với chỉ ~3 bug riêng biệt cho mỗi công cụ. Hai checker này phần lớn trùng lặp nhau.

Con số từ ngành: 38% (kèm cảnh báo)

Brie Bunge tại Airbnb ước tính tại JSConf Hawaii 2019 rằng 38% bug gần đây của Airbnb có thể phòng ngừa được với TypeScript. Con số đó đã được trích dẫn nhiều lần kể từ đó.

Cảnh báo: quy mô mẫu, khoảng thời gian, và phương pháp phân loại không được tiết lộ trong bài nói chuyện. Phân tích là nội bộ và chưa qua phản biện. Nó xuất hiện trước khi Airbnb áp dụng TypeScript trưởng thành; strict mode có thể chưa được bật trên code được phân tích. Khoảng cách giữa 38% và 15% từ học thuật là lớn và chưa được giải thích — các lý giải có thể bao gồm Airbnb đếm bug JS trong codebase được typed một phần (tiêu chuẩn thấp hơn), bộ lọc mức độ nghiêm trọng khác nhau, và định nghĩa “có thể phòng ngừa” khác nhau.

Dùng 38% làm động lực định hướng, không phải đo lường chính xác. Con số nghiêm ngặt là 15%.

Nghiên cứu repo 2022 phát hiện gì — và tại sao phức tạp hơn tưởng

Bogner và Merkel (arXiv:2203.11115) khai thác 604 repo GitHub — 299 JavaScript, 305 TypeScript — với 16 triệu dòng code. Họ đo chất lượng code, độ phức tạp, tỷ lệ commit sửa bug, và thời gian giải quyết bug.

Kết quả khó chịu: dự án TypeScript có chất lượng code tốt hơn đáng kể (ít code smell hơn, độ phức tạp thấp hơn) nhưng tỷ lệ commit sửa bug cao hơn — 0.206 so với 0.126 của dự án JavaScript.

Diễn giải của tác giả: “Ảnh hưởng tích cực được nhìn nhận của TypeScript trong việc tránh bug so với JavaScript có thể phức tạp hơn giả định.” Giả thuyết của họ cho sự khác biệt là các yếu tố gây nhiễu — dự án TypeScript trên GitHub có xu hướng lớn hơn và phức tạp hơn, điều này tự nhiên đẩy số bug được theo dõi lên cao hơn.

Thêm hai cảnh báo: nghiên cứu không kiểm soát mức độ strictness (codebase TypeScript dùng any nhiều thực chất không dùng type safety), và mức dùng any cao hơn tương quan với chỉ số chất lượng tệ hơn (Spearman’s ρ = 0.17–0.26) nhưng không tương quan cụ thể với tỷ lệ commit sửa bug.

TypeScript project thực sự thất bại ở đâu hiện nay

Một nghiên cứu phân loại 2026 (arXiv:2601.21186) xem xét 633 báo cáo bug đã được xác nhận từ 16 repository TypeScript mã nguồn mở phổ biến. Các danh mục bug theo tần suất:

  1. Lỗi tooling và cấu hình — danh mục chiếm ưu thế
  2. Dùng API sai cách
  3. Lỗi xử lý async / promise
  4. Lỗi logic và cú pháp — giảm so với mức cơ sở JavaScript
  5. Lỗi runtime liên quan đến type — cũng giảm so với JavaScript

Phát hiện chính: “Lỗi TypeScript hiện đại thường xuất hiện ở ranh giới tích hợp và điều phối hơn là trong logic thuật toán.” Strict typing đã thay đổi điểm thất bại của TypeScript project. Lỗi type và null ít gặp hơn. Build system, async boundary, và API contract vẫn còn rộng mở.

Những gì nó không bắt được

Hình dạng dữ liệu runtime. TypeScript type bị xóa lúc compile. Nếu API trả về { name: null } trong khi type của bạn khai báo { name: string }, TypeScript sẽ không bắt được. Dùng Zod, Valibot, hoặc io-ts để validate runtime.

Lỗi logic. Off-by-one, thuật toán sai, business rule sai — tất cả đều vô hình với type checker. TypeScript suy luận về những type nào chảy qua code, không phải code làm gì.

Type assertions. Từ khóa as bỏ qua kiểm tra type hoàn toàn. Strict mode không hạn chế as. Codebase dùng as thoải mái sẽ tích lũy những lời nói dối thầm lặng với type system.

any tường minh. noImplicitAny ngăn any ngầm định, không ngăn any tường minh. Bạn vẫn có thể viết const x: any = ... và mất toàn bộ type safety. Strict mode không ngăn any có chủ đích.

Bug async và promise. TypeScript type promise và async function đúng cách nhưng không thể bắt các rejection chưa được xử lý ở ranh giới chuỗi, race condition, hoặc hành vi phụ thuộc thời gian. Nghiên cứu phân loại 2026 liệt kê bug async/promise là danh mục lớn thứ ba trong TypeScript project.

Tooling và cấu hình build. Danh mục bug lớn nhất trong phân loại 2026. Strictness không có tác dụng gì với lỗi cấu hình webpack, lỗi path mapping trong tsconfig, hoặc @types version không khớp. Để xem cụ thể những lỗi build nào thường xuyên ảnh hưởng TypeScript monorepo trên production, xem Những cạm bẫy monorepo chúng tôi học được qua thực tiễn.

Truy cập index mà không có noUncheckedIndexedAccess. arr[99] trên mảng 3 phần tử có type là T, không phải T | undefined, trừ khi bạn thêm flag này riêng. Đây là nguồn phổ biến nhất của lỗi Cannot read properties of undefinedstrict: true vẫn không bắt được.

Đây là khoảng cách trông như thế nào trong code:

type User = { name: string; email?: string };
const users: Record<string, User> = {
  alice: { name: "Alice", email: "[email protected]" },
};

// WITHOUT strict
function greetLegacy(userId: string) {
  const user = users[userId];   // typed as User — not User | undefined
  return `Hello, ${user.name}`; // compiles cleanly, crashes at runtime for unknown userId
}

function sendEmailLegacy(user: User) {
  return user.email.toUpperCase(); // compiles cleanly, crashes: email is optional
}

// WITH strict: true
function greetStrict(userId: string) {
  const user = users[userId];
  // Still typed as User without noUncheckedIndexedAccess — same crash risk
  // With noUncheckedIndexedAccess: typed as User | undefined ↓
  if (!user) return "Unknown user";
  return `Hello, ${user.name}`; // safe
}

function sendEmailStrict(user: User) {
  // Error TS2532: Object is possibly 'undefined'
  // user.email.toUpperCase() — caught at compile time
  return user.email?.toUpperCase() ?? "(no email)"; // correct pattern
}

// strictFunctionTypes: parameters checked contravariantly
type Handler = (event: MouseEvent) => void;
// Without strictFunctionTypes: (e: Event) => void silently accepted as Handler
// With strictFunctionTypes: compile error — Event is wider than MouseEvent

// noImplicitAny: forces explicit annotation
// function process(data) { return data.value * 2; }  — Error TS7006
function processStrict(data: { value: number }) {
  return data.value * 2;
}

Chi phí migration

Các con số là thực tế.

Tổ chứcPhạm viLỗi khi migration
Figma~1,162 file, ~376k LoC4,000+ lỗi (chỉ thêm strictNullChecks)
Dự án lớn thông thườngDự án 4 năm tuổi~4,300 lỗi (full strict: true)
Slack DesktopMigration 6 thángTìm thấy “nhiều bug nhỏ”

Trường hợp của Figma (Figma Engineering Blog) là tài liệu sơ cấp chi tiết nhất: 30 kỹ sư, hai sprint 3 ngày, 4,000+ lỗi trên 1,162 file chỉ từ việc bật strictNullChecks. Kết luận của họ: “Một số sự cố nghiêm trọng của chúng tôi đã có thể được bắt trước khi lên production” và sau migration “lỗi null không còn xuất hiện trên error dashboard của chúng tôi.” Không có thống kê tỷ lệ bug trước/sau được công bố, nhưng tín hiệu định tính là tích cực.

Từng flag một tốt hơn bật tất cả cùng lúc

Với codebase lớn, thứ tự migration được khuyến nghị:

  1. noImplicitAny trước — có thể làm từng file với // @ts-check trước khi chạm vào tsconfig.json
  2. strictNullChecks tiếp theo — tạo ra nhiều lỗi nhất nhưng mang lại giá trị lớn nhất
  3. Các flag strict còn lại — ít ma sát hơn, thêm chúng cùng lúc

Bật strict: true tất cả cùng lúc trên codebase 300k dòng tạo ra bế tắc kéo dài cả quý. Phương pháp từng bước của Figma — compile hai lần, kiểm soát theo dependency graph — đã được ghi lại và có thể tái tạo.

Hầu hết lỗi xuất hiện trong quá trình migration là bug thực, không phải false positive. strictPropertyInitialization là ngoại lệ chính: nó tạo ra false positive thực sự khi khởi tạo xảy ra bên ngoài constructor (Angular lifecycle hook, dependency injection framework). Hãy tính đến điều này khi phân loại.

IDE enforcement thu hẹp khoảng cách phản hồi

JetBrains WebStorm hiển thị lỗi TypeScript strict ngay khi bạn gõ, không cần bước compile riêng. Nếu bạn làm việc trong team mà không phải ai cũng chạy tsc --watch liên tục, IDE-level enforcement thu hẹp khoảng cách phản hồi — lỗi xuất hiện ngay lúc viết, không phải ở CI. Điều này quan trọng nhất trong quá trình migration khi mục tiêu là loại bỏ backlog lỗi từng file một.

Kết luận

Độ tin cậy cao — có thể trích dẫn trực tiếp:

  • TypeScript strict mode bắt được ~15% bug JavaScript được công bố đã qua nghiên cứu độc lập (Gao et al. 2017, n=400).
  • Bật strictNullChecks trên codebase thực tế phát hiện hàng trăm đến hàng nghìn lỗi thực, không phải false positive (báo cáo sơ cấp từ Figma, Slack).
  • Phân loại 2026 cho thấy strict typing đã thay đổi điểm thất bại của TypeScript project — lỗi type và null ít gặp hơn; tooling, async, và API misuse là các chế độ thất bại chiếm ưu thế hiện nay.
  • TypeScript 6.0 đặt strict: true làm mặc định. Ngành đã quyết định.

Độ tin cậy trung bình — cần kèm cảnh báo:

  • Con số 38% của Airbnb có thể hợp lý nhưng mờ về phương pháp. Coi nó như động lực minh họa, không phải dữ liệu chính xác để trích dẫn.
  • TypeScript project không có ít commit sửa bug hơn về mặt thống kê so với JavaScript project (Bogner & Merkel 2022), nhưng kết quả này bị ảnh hưởng bởi độ phức tạp của dự án và hiệu ứng chọn lọc.

Thành thật “chúng tôi không biết”:

  • Không có nghiên cứu kiểm soát quy mô lớn nào để so sánh strict: truestrict: false trong cùng codebase theo thời gian.
  • Không có dữ liệu khảo sát về tỷ lệ phần trăm TypeScript project thực sự dùng strict: true.
  • Các thay đổi tỷ lệ bug production dài hạn sau migration vẫn chỉ là giai thoại.

Khuyến nghị: bật strict: true. Thêm noUncheckedIndexedAccess riêng nếu bạn có thể chịu được khối lượng lỗi ban đầu — nó bắt những bug mà strict vẫn bỏ qua. Đừng kỳ vọng tỷ lệ bug giảm một phần ba; hãy kỳ vọng cải thiện đáng kể trong xử lý null/undefined và một bề mặt type mà những người đóng góp trong tương lai có thể thực sự tin tưởng.

Nếu bạn đang cân nhắc giữa TypeScript và Go cho backend service mới, Go vs TypeScript — lựa chọn ngôn ngữ backend cho năm 2026 có benchmark throughput và phân tích tradeoff theo cấu trúc team.

Tài liệu tham khảo

NguồnLoại
Gao et al. 2017 (ICSE) — To Type or Not to TypeNghiên cứu độc lập
Bogner & Merkel 2022 — arXiv:2203.11115Preprint
Bugs in TS Ecosystem 2026 — arXiv:2601.21186Preprint
Brie Bunge, JSConf Hawaii 2019 — Adopting TypeScript at ScaleBài nói chuyện hội nghị
Figma — Inside Figma: a case study on strict null checksBlog kỹ thuật
TypeScript Design GoalsTài liệu chính thức
TypeScript TSConfig ReferenceTài liệu chính thức
GitHub issue #49169 — noUncheckedIndexedAccess in strictGitHub issue
TypeScript 6.0 Release NotesTài liệu chính thức