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, nullundefined 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 ES2021

Khuyế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ần import React mỗ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 build
  • forceConsistentCasingInImports: Tránh bug trên Linux (case-sensitive filesystem)
  • noFallthroughCasesInSwitch: Bắt lỗi quên break trong 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
  • strictNullChecks là flag quan trọng nhất trong strict family
  • moduleResolution phải phù hợp với bundler/runtime bạn dùng
  • target quyết định output JS, lib quyết định type definitions có sẵn
  • paths chỉ là type-level, phải config thêm cho bundler
  • Luôn dùng isolatedModules nế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.