tsconfig sâu
Bài 9: tsconfig sâu
Bạn đã bao giờ mở một dự án mới, chạy tsc --init, rồi... để nguyên mặc định chưa? Hoặc tệ hơn — copy-paste tsconfig từ dự án cũ mà không hiểu từng dòng có nghĩa gì?
Đây là bài học giúp bạn hiểu từng flag quan trọng trong tsconfig.json, tại sao chúng tồn tại, và cách cấu hình đúng cho production.
#Vấn đề: strict mode bị tắt
Nhiều dev mới tắt strict vì "TypeScript báo lỗi nhiều quá". Đây là sai lầm lớn nhất.
// ❌ Với strict: false — TypeScript "im lặng" trước lỗi nguy hiểm
function getLength(value: string | null) {
return value.length; // Không báo lỗi! Runtime crash khi value = null
}
const user = { name: "An", age: 25 };
console.log(user.email); // Không báo lỗi! Trả về undefined thay vì cảnh báo// ✅ Với strict: true — TypeScript bắt lỗi ngay tại compile time
function getLength(value: string | null) {
// ❌ Error: 'value' is possibly 'null'
return value.length;
// ✅ Phải kiểm tra trước
if (value === null) return 0;
return value.length;
}Anti-pattern: "strict": false — Đây là cách nhanh nhất để biến TypeScript thành "JavaScript có thêm bước build". Bạn đang bỏ đi 80% giá trị của TypeScript.
#strict Family Flags — Hiểu từng flag
`"strict": true" thực chất là bật đồng thời tất cả các flag sau:
#strictNullChecks
Flag quan trọng nhất. Khi bật, null và undefined không thể gán cho các type khác unless bạn explicitly cho phép.
// ❌ strictNullChecks: false
let name: string = null; // OK — đây là bug tiềm ẩn
// ✅ strictNullChecks: true
let name: string = null; // ❌ Error
let nameOrNull: string | null = null; // ✅ OK#noImplicitAny
Bắt buộc phải khai báo type khi TypeScript không thể infer.
// ❌ noImplicitAny: false
function process(data) { // 'data' implicitly has 'any' type
return data.whatever.you.want; // Không có type checking nào
}
// ✅ noImplicitAny: true
function process(data: unknown) {
// Phải narrowing trước khi dùng
if (typeof data === "object" && data !== null) {
// ...
}
}#strictFunctionTypes
Kiểm tra contravariance cho function parameter types. Flag này bắt lỗi mà bạn không ngờ tới.
type Handler = (event: MouseEvent) => void;
// ❌ strictFunctionTypes: false — Cho phép gán không an toàn
const handler: Handler = (event: Event) => { // OK (sai!)
console.log(event.clientX); // Runtime crash — Event không có clientX
};
// ✅ strictFunctionTypes: true
const handler: Handler = (event: Event) => {
// ❌ Error: Type '(event: Event) => void' is not assignable to type '(event: MouseEvent) => void'
};#strictBindCallApply
Kiểm tra type cho .bind(), .call(), .apply().
function greet(name: string, age: number) {
return `${name} is ${age}`;
}
// ❌ strictBindCallApply: false
greet.call(null, "An"); // Không báo lỗi thiếu argument
// ✅ strictBindCallApply: true
greet.call(null, "An"); // ❌ Error: Expected 3 arguments, but got 2#strictPropertyInitialization
Đảm bảo class properties được khởi tạo trong constructor.
// ❌ strictPropertyInitialization: false
class User {
name: string; // Không báo lỗi dù chưa gán giá trị
constructor() {}
}
// ✅ strictPropertyInitialization: true
class User {
name: string; // ❌ Error: Property 'name' has no initializer
constructor(name: string) {
this.name = name; // ✅ OK
}
}#noImplicitThis
Báo lỗi khi this có type any.
// ❌ noImplicitThis: false
const obj = {
name: "An",
greet() {
return function () {
return this.name; // 'this' là any — không an toàn
};
},
};
// ✅ noImplicitThis: true
const obj = {
name: "An",
greet() {
return function (this: typeof obj) {
return this.name; // ✅ Rõ ràng
};
},
};#alwaysStrict
Emit "use strict" trong mọi file JS output. Luôn bật flag này.
#moduleResolution — Chọn đúng chiến lược
Đây là flag gây nhầm lẫn nhất. Mỗi giá trị thay đổi cách TypeScript tìm file import.
#node (legacy)
Mô phỏng Node.js CommonJS resolution cũ. Tìm index.js trong thư mục.
{
"moduleResolution": "node"
}import "./utils" → tìm ./utils.ts, ./utils.tsx, ./utils.d.ts, ./utils/index.ts#node16 / nodenext
Resolution theo Node.js ESM thực sự. Bắt buộc dùng extension trong import.
{
"moduleResolution": "node16"
}// ❌ Không có extension — lỗi
import { helper } from "./utils";
// ✅ Phải có .js extension (dù file thật là .ts)
import { helper } from "./utils.js";#bundler
Dành cho project dùng bundler (Vite, esbuild, Webpack). Không bắt buộc extension.
{
"moduleResolution": "bundler"
}// ✅ Không cần extension — bundler sẽ resolve
import { helper } from "./utils";Khuyến nghị:
- React/Vite/frontend project →
bundler - Node.js backend ESM →
node16 - Legacy CommonJS →
node
#target vs lib — Không phải là một
#target
Quyết định output JavaScript version. TypeScript sẽ downlevel syntax.
{ "target": "ES2020" }// Input
const result = await fetch("/api");
const data = result?.data ?? "default";
// target: ES5 → TypeScript compile optional chaining thành if-else
// target: ES2020 → Giữ nguyên syntax hiện đại#lib
Quyết định type definitions nào được bao gồm. Không ảnh hưởng output.
{
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"]
}// ✅ Có "DOM" trong lib → biết về document, window
document.getElementById("app");
// ✅ Có "ES2020" trong lib → biết về Promise.allSettled
Promise.allSettled([p1, p2]);
// ❌ Không có "ES2021" trong lib → không biết về Promise.any
Promise.any([p1, p2]); // Error nếu lib thiếu ES2021Khuyến nghị:
- Frontend:
target: "ES2020",lib: ["ES2020", "DOM", "DOM.Iterable"] - Node.js:
target: "ES2022",lib: ["ES2022"](Node có API riêng, không cần DOM)
#paths — Aliasing imports
Tạo alias để tránh ../../../../ hell.
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"]
}
}
}// ❌ Không có paths
import { Button } from "../../../components/Button";
import { formatDate } from "../../../utils/date";
// ✅ Có paths
import { Button } from "@components/Button";
import { formatDate } from "@utils/date";Lưu ý quan trọng: paths chỉ là type-level alias. Runtime bundler phải tự resolve. Cần config thêm trong Vite/Webpack:
// vite.config.ts
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
}#jsx Setting
{
"jsx": "react-jsx" // React 17+ — không cần import React
}Các giá trị phổ biến:
"react-jsx"— React 17+ automatic runtime"react-jsxdev"— Như trên nhưng có dev warnings"react"— React 16, cầnimport Reactmỗi file"preserve"— Giữ nguyên JSX, để bundler xử lý
#Cấu hình production mẫu
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}Giải thích các flag hay bị bỏ qua:
isolatedModules: Bắt buộc nếu dùng esbuild/Vite (mỗi file compile riêng)skipLibCheck: Bỏ qua kiểm tra .d.ts files → tăng tốc buildforceConsistentCasingInImports: Tránh bug trên Linux (case-sensitive filesystem)noFallthroughCasesInSwitch: Bắt lỗi quênbreaktrong switch-case
#Bài tập
#Dễ
Cho tsconfig sau, tìm và sửa lỗi: "strict": false, "moduleResolution": "node", không có "lib".
#Trung bình
Viết tsconfig cho dự án React + Vite với strict mode, path aliases, và JSX automatic runtime.
#Khó
Giải thích tại sao "moduleResolution": "node16" yêu cầu .js extension trong import dù file thật là .ts. Viết code minh họa.
#Tổng kết
- `"strict": true" luôn luôn bật — không có lý do nào để tắt trong project mới
strictNullCheckslà flag quan trọng nhất trong strict familymoduleResolutionphải phù hợp với bundler/runtime bạn dùngtargetquyết định output JS,libquyết định type definitions có sẵnpathschỉ là type-level, phải config thêm cho bundler- Luôn dùng
isolatedModulesnếu build tool là esbuild/Vite
Bài tiếp theo: Declaration Files (.d.ts) — cách viết type cho thư viện JavaScript không có TypeScript.