TypeScript Production · Phần 2 — Strict tsconfig & Compiler Mental Model
Thiết kế tsconfig theo runtime và toolchain, bật strict flags có chủ đích, hiểu target/lib/module, noEmit, isolatedModules và những thay đổi quan trọng của TypeScript 6.
tsconfig.json không phải file copy từ Stack Overflow. Nó là bản mô tả ba sự thật: code chạy ở runtime nào, module được resolve ra sao, và mức rủi ro team chấp nhận.
Compiler làm ba việc khác nhau
- Xây program graph từ
files/includevà import. - Type-check graph theo compiler options.
- Có thể emit JavaScript, declaration và source map.
Bundler có thể làm bước 3 nhưng không thay tsc ở bước 2. Với app dùng Vite/Astro/Next, pattern phổ biến là bundler emit, tsc --noEmit kiểm type.
Baseline cho app hiện đại
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"noEmit": true,
"skipLibCheck": true
},
"include": ["src", "tests"]
}
Đây là điểm bắt đầu, không phải config phổ quát. Node library nên dùng NodeNext; package cần emit declaration không thể noEmit; Web Worker không nên vô tình có toàn bộ DOM globals.
target, lib, module không cùng một nút
target: syntax JavaScript màtscemit.lib: API ambient mà compiler cho là tồn tại.module: hình thức module emit và một phần hành vi resolution.
Thêm DOM chỉ làm document có type, không polyfill document. Đặt target: ESNext không chứng minh production runtime hỗ trợ mọi API mới.
Những strict flag biến bug im lặng thành thiết kế rõ
const counts: Record<string, number> = {};
counts['missing'].toFixed();
// noUncheckedIndexedAccess => Object is possibly undefined
type Patch = { name?: string };
const patch: Patch = { name: undefined };
// exactOptionalPropertyTypes => lỗi; "vắng" khác "có key với undefined"
skipLibCheck bỏ kiểm tra nội bộ .d.ts của dependency để giảm thời gian, nhưng type của dependency vẫn được dùng khi kiểm tra code bạn. Đừng dùng nó để giải quyết hai bản type không tương thích mà không hiểu dependency graph.
TypeScript 6: migration phải explicit hơn
TypeScript 6 chuẩn bị đường sang native compiler TypeScript 7 và bỏ/deprecate nhiều giả định cũ. Hai điều đáng đưa vào config review:
- khai báo
typesrõ, ví dụ"types": ["node"], thay vì để mọi@types/*tự tràn vào global scope; - đặt
rootDirrõ cho package emit, đặc biệt khi dùng packageimports/exports.
Không thêm ignoreDeprecations rồi quên. Nếu cần dùng tạm, tạo issue có owner và deadline trước lần nâng major tiếp theo.
Config theo layer, không copy-paste
tsconfig.base.json # strict policy chung
apps/web/tsconfig.json # DOM + bundler + noEmit
apps/api/tsconfig.json # Node + NodeNext
packages/core/tsconfig.json # composite + declaration
extends kế thừa option, nhưng đường dẫn tương đối được resolve theo file khai báo chúng. Chạy tsc --showConfig -p ... khi config thực tế khác điều bạn tưởng.
Debug compiler như senior
tsc --showConfig -p tsconfig.json
tsc --explainFiles -p tsconfig.json
tsc --traceResolution -p tsconfig.json
tsc --extendedDiagnostics -p tsconfig.json
Đừng bật traceResolution rồi đọc từ đầu. Tìm module đang lỗi, xem candidate path, condition và lý do reject.
Lab
- Tách config browser và Node; bảo đảm browser code không dùng
processdo ambient type rò rỉ. - Bật từng
noUncheckedIndexedAccess,exactOptionalPropertyTypes,noUncheckedSideEffectImports; sửa lỗi theo domain thay vì assertion. - Ghi ADR một trang: runtime target, module mode, ai emit JS, ai type-check.
- Lưu
--extendedDiagnosticslàm baseline trước và sau thay đổi.
Done khi: một contributor mới có thể giải thích từng non-default flag và CI chạy đúng config cho từng runtime.