Tạo tsconfig.json: chọn môi trường đích rồi tinh chỉnh từng cờ biên dịch
Công cụ dựng tệp tsconfig.json theo năm môi trường thường gặp là Next.js, React SPA, Node.js backend, thư viện npm và gói trong monorepo, sau đó cho bạn bật tắt từng cờ với một dòng giải thích ngay bên dưới. Khi hai lựa chọn đá nhau, chẳng hạn declaration bật cùng noEmit hay moduleResolution Bundler đi với module CommonJS, khối cảnh báo nêu rõ chuyện gì sẽ xảy ra thay vì chỉ tô đỏ.
Tính năng nổi bật
- Năm bộ dựng sẵn theo môi trường đích, mỗi bộ đặt sẵn cả nhóm cờ đi liền nhau chứ không chỉ vài dòng lẻ
- Mọi cờ đều kèm một dòng giải thích tiếng Việt hiện ngay cạnh, không phải mở tài liệu tra riêng
- Công tắc strict kéo theo cả tám cờ con, tắt riêng từng cờ vẫn giữ được strict ở mức tổng
- Nhóm kiểm tra bổ sung ngoài strict gồm noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride
- Khối cảnh báo rà hơn hai mươi tổ hợp mâu thuẫn và phân biệt lỗi cấu hình thật với lựa chọn đáng ngờ
- Bảng khai alias thêm bớt được từng dòng, kiểm tra dấu sao ở hai vế có khớp nhau không
- Chỉ ghi vào tệp những cờ khác mặc định nên cấu hình sinh ra ngắn, dễ đọc lại sau vài tháng
- Chạy hoàn toàn trong trình duyệt, gõ tới đâu tệp đổi tới đó và có nút chép nguyên tệp
Vì sao chép tsconfig của dự án khác thường gây rắc rối
Tệp tsconfig.json là thứ hầu hết lập trình viên chép từ một dự án cũ rồi sửa cho tới khi hết báo lỗi, và đó chính là gốc của phần lớn vấn đề về sau. Một cấu hình chép từ dự án Node backend mang theo module CommonJS và lib không có DOM, đưa sang dự án React thì trình biên dịch không hiểu window, còn bundler thì không tách được mã theo mô đun. Ngược lại, cấu hình chép từ dự án Next.js mang theo noEmit, đưa sang gói npm thì lệnh build chạy xong không sinh ra tệp nào cả và bạn ngồi tìm nguyên nhân trong nửa buổi. Rắc rối lớn hơn là những cờ ảnh hưởng đến chất lượng mã, đặc biệt là strict và các cờ con: một dự án tắt strictNullChects từ ngày đầu rồi sau hai năm mới muốn bật sẽ nhận về hàng nghìn lỗi cùng lúc, đến mức không ai dám bật nữa. Chọn đúng ngay từ tệp đầu tiên rẻ hơn rất nhiều so với sửa sau. Công cụ này bày ra đúng những lựa chọn đó kèm hệ quả của từng lựa chọn, để bạn quyết định có cơ sở chứ không chép mù.
Lợi ích khi sử dụng
- Biết ngay hệ quả của từng cờ thay vì mở tài liệu tiếng Anh tra từng dòng
- Tránh những tổ hợp mâu thuẫn chỉ lộ ra khi chạy lệnh build trên máy chủ
- Cấu hình sinh ra ngắn gọn vì bỏ hết cờ trùng mặc định, người sau đọc là hiểu
- Chọn đúng mức nghiêm ngặt từ đầu, không phải trả giá khi dự án đã lớn
- Phân tách rõ việc của trình biên dịch với việc của ESLint và Prettier
Cách tạo tsconfig.json bằng công cụ này
- 1Bấm một trong năm nút môi trường đích ở khối trên cùng, cấu hình nền tương ứng được nạp ngay và dòng mô tả bên dưới nói rõ vì sao bộ đó đặt như vậy.
- 2Xem lại nhóm ngôn ngữ và mô đun: target quyết định cú pháp hạ xuống mức nào, module và moduleResolution phải khớp nhau, lib quyết định API nào được coi là có sẵn.
- 3Quyết định mức nghiêm ngặt: để strict bật cho dự án mới, hoặc bật strict rồi tắt từng cờ con nếu bạn đang chuyển dần một dự án JavaScript cũ.
- 4Khai đường dẫn rút gọn trong khối paths nếu dự án dùng alias, nhớ rằng dấu sao phải có ở cả vế trái lẫn vế phải.
- 5Đọc khối cảnh báo, xử lý hết các mục nghiêm trọng rồi bấm chép cấu hình và dán vào tệp tsconfig.json ở thư mục gốc dự án.
tsconfig quyết định trình biên dịch hiểu mã thế nào, không quyết định mã trông ra sao
Ba tệp cấu hình hay nằm cạnh nhau trong một dự án frontend là tsconfig.json, cấu hình ESLint và cấu hình Prettier, và ranh giới giữa chúng cần được giữ rất rõ. tsconfig trả lời câu hỏi trình biên dịch hiểu mã của bạn thế nào: cú pháp nào hợp lệ, một biểu thức mang kiểu gì, đường dẫn nhập trỏ tới tệp nào, mã phát sinh ra ở định dạng mô đun nào. Nó không quan tâm bạn viết nháy đơn hay nháy kép, cũng không quan tâm bạn để hàm dài bao nhiêu dòng. ESLint trả lời câu hỏi khác hẳn: quy ước và lỗi logic nào bị báo, ví dụ gọi hook trong vòng lặp, để biến trong khối phụ thuộc của useEffect thiếu, so sánh bằng hai dấu bằng, nhập vòng giữa hai mô đun. Prettier chỉ trả lời một câu hỏi duy nhất là mã trông thế nào, và nó viết lại toàn bộ cách xuống dòng, thụt lề, đặt dấu ngoặc theo đúng bộ luật của mình. Có vài chỗ giao nhau khiến người ta nhầm. Cờ noUnusedLocals trong tsconfig và quy tắc no-unused-vars trong ESLint cùng bắt một loại lỗi, nhưng ESLint xử lý mềm hơn vì bỏ qua được tham số bắt đầu bằng gạch dưới và tự xóa được dòng nhập thừa, nên nhiều đội tắt cờ trong tsconfig và giao việc đó cho ESLint. Ngược lại, không quy tắc ESLint nào thay được strictNullChecks vì đó là chuyện của hệ thống kiểu chứ không phải chuyện quy ước.
module, moduleResolution và target: ba cờ hay bị đặt lệch nhau nhất
Ba cờ này thường bị nhầm là ba cách nói về cùng một thứ, nhưng chúng trả lời ba câu hỏi tách biệt. Cờ target nói mã phát sinh dùng cú pháp của phiên bản JavaScript nào: để ES5 thì lớp, hàm mũi tên và bất đồng bộ đều bị viết lại thành cú pháp cũ, khiến tệp phồng lên rất nhiều. Cờ module nói tệp phát sinh dùng hệ mô đun nào: CommonJS sinh ra require và exports, các bản ES giữ nguyên import và export, còn NodeNext để Node tự quyết theo trường type trong package.json và theo đuôi tệp là mjs hay cjs. Cờ moduleResolution nói cách trình biên dịch đi tìm tệp khi bạn viết một đường dẫn nhập, và đây mới là chỗ hay lỗi. Giá trị Bundler ra đời cho các dự án có Vite hay webpack lo phần đóng gói, nó cho phép nhập không cần ghi đuôi tệp và đọc được trường exports trong package.json, nhưng nó chỉ hợp lệ khi module từ ES2015 trở lên hoặc Preserve. Đặt Bundler cùng CommonJS là lỗi cấu hình chứ không phải lựa chọn dở. Tương tự, NodeNext và Node16 bắt buộc module cùng tên, vì cách phân giải của Node phụ thuộc trực tiếp vào định dạng mô đun đang dùng. Quy tắc dễ nhớ: mã chạy qua bundler thì đặt module ESNext với moduleResolution Bundler, mã chạy thẳng trên Node thì đặt cả hai thành NodeNext.
strict và tám cờ con: bật hết hay bật dần
Bật strict là bật cùng lúc tám cờ: noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables và alwaysStrict. Với dự án mới, không có lý do gì để tắt bất cứ cờ nào trong số đó, vì chi phí sửa từng lỗi khi mã còn ít gần như bằng không. Với dự án JavaScript đang chuyển dần sang TypeScript thì bật hết một lần sẽ cho ra hàng nghìn lỗi và cả đội bỏ cuộc, nên cách làm hợp lý là bật strict ở mức tổng rồi tắt riêng vài cờ nặng nhất, thường là strictNullChecks và strictPropertyInitialization, và ghi vào tài liệu dự án ngày dự kiến bật lại. Trong tám cờ đó, strictNullChecks là cờ đáng giá nhất và cũng khó nuốt nhất: nó tách null và undefined ra khỏi mọi kiểu khác, nên mọi chỗ đọc thuộc tính của giá trị có thể rỗng đều phải kiểm tra trước. Đổi lại, nó xóa sổ gần như toàn bộ lỗi đọc thuộc tính của undefined lúc chạy, tức nhóm lỗi phổ biến nhất trong ứng dụng web. Cần lưu ý strictPropertyInitialization phụ thuộc strictNullChecks, tắt cái sau mà giữ cái trước là lỗi cấu hình. Ngoài tám cờ này còn nhóm kiểm tra bổ sung không nằm trong strict, đáng chú ý nhất là noUncheckedIndexedAccess: nó khiến mọi phép truy cập theo chỉ mục trả về kiểu kèm undefined, phản ánh đúng thực tế là phần tử thứ mười của mảng bảy phần tử không tồn tại.
declaration, noEmit và composite: ba cờ quyết định dự án có sinh ra tệp hay không
Đây là nhóm cờ gây nhầm lẫn nhiều nhất khi chép cấu hình giữa các dự án. Cờ noEmit nói với TypeScript rằng chỉ kiểm tra kiểu thôi, đừng ghi tệp nào cả, và đó là cách làm đúng với dự án Next.js hay Vite vì phần dịch đã do SWC hoặc esbuild lo. Trong những dự án đó, lệnh kiểm tra kiểu thường là tsc với cờ noEmit chạy riêng trong quy trình tích hợp, còn lệnh build của khung ứng dụng không gọi tsc. Cờ declaration nói TypeScript sinh thêm tệp .d.ts mô tả kiểu công khai, và với gói phát hành lên npm thì đây là cờ bắt buộc, thiếu nó người cài gói về sẽ thấy toàn any. Hai cờ này loại trừ nhau: bật cả hai thì không tệp khai báo nào được ghi ra, và đó là lỗi mà công cụ này cảnh báo riêng vì nó im lặng, không có thông báo nào lúc build. Cờ composite dành cho monorepo, nó biến gói hiện tại thành một dự án được phép tham chiếu từ gói khác qua trường references, cho phép TypeScript dựng lại theo đồ thị phụ thuộc và chỉ dịch những gói thật sự đổi. Composite bắt buộc declaration phải bật, tự động bật incremental, và không đi cùng noEmit được. Còn declarationMap là cờ nhỏ nhưng đáng bật với thư viện nội bộ: nó khiến lệnh nhảy tới định nghĩa trong trình soạn thảo mở đúng tệp nguồn TypeScript thay vì dừng ở tệp khai báo.
isolatedModules và verbatimModuleSyntax: hai cờ sinh ra vì công cụ dịch từng tệp riêng lẻ
Trình biên dịch của TypeScript nhìn được toàn bộ dự án cùng lúc, nên nó biết một tên được nhập vào là kiểu hay là giá trị thật và tự bỏ đi những dòng nhập chỉ dùng cho kiểu. Các công cụ dịch nhanh như esbuild, SWC và Babel thì khác hẳn: chúng xử lý từng tệp một cách độc lập, không biết gì về tệp bên cạnh, nên có những cú pháp chúng không thể xử lý đúng. Cờ isolatedModules bật lên để TypeScript báo lỗi ngay tại những cú pháp đó, ví dụ nhập một kiểu rồi xuất lại nó bằng câu lệnh xuất thường mà không ghi rõ từ khóa type, hoặc khai báo const enum. Có isolatedModules, bạn phát hiện vấn đề lúc kiểm tra kiểu thay vì nhận một tệp dịch sai ở môi trường thật. Cờ verbatimModuleSyntax đi xa hơn một bước: nó buộc TypeScript giữ nguyên mọi câu lệnh nhập và xuất đúng như bạn viết, không tự xóa dòng nào. Hệ quả trực tiếp là mọi thứ chỉ dùng làm kiểu bắt buộc phải viết import type, nếu không dòng nhập đó sẽ còn nguyên trong tệp phát sinh và kéo theo một mô đun thừa lúc chạy, đôi khi gây lỗi vì mô đun đó có tác dụng phụ. Đây là cờ nên bật với dự án dùng bundler hiện đại và với thư viện phát hành, nhưng cần cân nhắc với module CommonJS vì khi đó bạn phải viết cú pháp CommonJS ngay trong tệp TypeScript.
Câu hỏi thường gặp (FAQ)
Đặt target bao nhiêu là hợp lý cho dự án web năm nay?
ES2020 hoặc ES2022 là mức phổ biến, vì mọi trình duyệt còn được cập nhật đều hiểu. Đặt ES5 chỉ cần thiết khi bạn buộc phải chạy trên trình duyệt rất cũ, và cái giá là tệp phát sinh phồng lên vì phải mô phỏng lớp, vòng lặp và cú pháp bất đồng bộ.
Vì sao đặt moduleResolution là Bundler lại bị báo lỗi?
Giá trị Bundler chỉ dùng được khi module là ES2015 trở lên hoặc Preserve. Nếu module đang để CommonJS thì TypeScript từ chối ngay. Sửa bằng cách đổi module sang ESNext, hoặc nếu mã chạy thẳng trên Node thì đặt cả module lẫn moduleResolution thành NodeNext.
Có nên bật strict cho dự án JavaScript đang chuyển dần sang TypeScript không?
Nên bật strict ở mức tổng rồi tắt riêng một hai cờ nặng nhất, thường là strictNullChecks, và ghi rõ trong tài liệu dự án ngày dự kiến bật lại. Bật hết một lần trên mã cũ sẽ cho ra hàng nghìn lỗi cùng lúc và cả đội sẽ bỏ luôn ý định dùng strict.
noUncheckedIndexedAccess có đáng bật không, nó làm mã dài thêm nhiều?
Đáng bật với mã xử lý dữ liệu từ ngoài vào, vì nó phản ánh đúng thực tế là phần tử tại một chỉ mục có thể không tồn tại. Với mã lặp mảng thuần túy thì nó gây phiền vì buộc bạn kiểm tra thừa. Nhiều đội bật nó ở gói xử lý dữ liệu và tắt ở gói giao diện.
Vì sao chạy build xong mà không thấy tệp nào trong thư mục dist?
Gần như chắc chắn là noEmit đang bật. Cờ này bảo TypeScript chỉ kiểm tra kiểu chứ không ghi tệp, đúng cho dự án Next.js hay Vite nhưng sai cho gói npm. Tắt noEmit rồi đặt outDir, và nếu là gói phát hành thì bật thêm declaration.
Khai paths trong tsconfig rồi mà chạy lên vẫn báo không tìm thấy mô đun?
Vì paths chỉ dạy cho trình biên dịch hiểu, còn công cụ đóng gói và trình chạy hoàn toàn không đọc nó. Bạn phải khai lại alias tương ứng ở nơi thứ hai: trường resolve.alias của webpack, mục resolve trong cấu hình Vite, hoặc trường imports trong package.json khi chạy thẳng bằng Node.
Khác nhau giữa include, exclude và files là gì?
files liệt kê từng tệp cụ thể, include nhận mẫu đường dẫn có dấu sao, exclude loại bớt khỏi kết quả của include chứ không loại được tệp nào đã ghi trong files. Cần nhớ khi bạn tự khai exclude thì danh sách mặc định bị thay hẳn, nên phải liệt kê lại node_modules.
skipLibCheck bật lên có che mất lỗi thật của tôi không?
Không. Nó chỉ bỏ qua việc kiểm tra kiểu bên trong các tệp .d.ts của thư viện, còn mã của bạn vẫn được kiểm tra đầy đủ khi dùng thư viện đó. Hầu hết dự án nên bật vì nó rút ngắn thời gian build rõ rệt và tránh việc một gói phụ thuộc khai báo ẩu làm hỏng cả bản dựng.
verbatimModuleSyntax bật lên rồi phải sửa những gì trong mã?
Mọi dòng nhập chỉ dùng làm kiểu phải viết thành import type, kể cả khi nhập lẫn kiểu và giá trị thì phần kiểu cũng nên tách ra. Đa số trình soạn thảo có lệnh sửa nhanh cho việc này, và ESLint có quy tắc consistent-type-imports tự sửa hàng loạt được.
Có nên bật noUnusedLocals hay để ESLint lo việc đó?
Nhiều đội để ESLint lo, vì quy tắc bên ESLint bỏ qua được tham số bắt đầu bằng dấu gạch dưới và có thể tự xóa dòng nhập thừa khi chạy chế độ sửa. Cờ trong tsconfig thì cứng hơn, nó chặn cả bản dựng chỉ vì một biến tạm chưa dùng tới, gây khó chịu khi đang viết dở.
composite trong monorepo có bắt buộc không?
Không bắt buộc, nhưng nếu bạn muốn dùng trường references để TypeScript dựng lại theo đồ thị phụ thuộc và chỉ dịch phần đổi thì bắt buộc phải có. Composite kéo theo declaration và incremental, nên đừng đặt cùng noEmit vì gói khác cần đọc tệp khai báo do gói này sinh ra.
Cấu hình sinh ra ngắn hơn tsconfig mẫu của khung ứng dụng, có thiếu gì không?
Không thiếu. Công cụ chỉ ghi những cờ khác giá trị mặc định, còn tệp mẫu của nhiều khung ứng dụng liệt kê cả cờ trùng mặc định cho dễ nhìn. Cấu hình ngắn thực ra dễ bảo trì hơn vì mỗi dòng còn lại đều là một quyết định có chủ đích của đội.
Từ khóa liên quan
- tạo tsconfig json
- tsconfig generator online
- cấu hình tsconfig cho next js
- tsconfig cho react vite
- tsconfig cho node backend
- tsconfig cho thư viện npm
- strict mode typescript là gì
- strictNullChecks là gì
- noUncheckedIndexedAccess
- moduleResolution bundler
- module nodenext khác gì commonjs
- verbatimModuleSyntax
- isolatedModules typescript
- paths alias trong tsconfig
- declaration d ts file
- composite project monorepo
- noEmit typescript
- skipLibCheck có nên bật
- target es2022 hay es5
- lỗi cấu hình tsconfig thường gặp
