← TypeScript Thực Chiến

Bài 10 · Vận dụng · 20 phút

tsconfig, strict & .d.ts

Biên soạn bởi Nguyễn Anh Tuấn

Những cấu hình tsc đáng quan tâm: nhóm strict, target / module / moduleResolution; tệp khai báo kiểu (.d.ts) và DefinitelyTyped (@types) để gắn kiểu cho thư viện JS chưa có kiểu; import type. Phần đời thực của một dự án TypeScript.

Một dự án TypeScript được điều khiển bởi tsconfig.json ở thư mục gốc. Chạy npx tsc --init để sinh bản đầy đủ kèm chú thích. Phần quan trọng nhất là compilerOptions:

tsconfig.json (rút gọn)

{
  "compilerOptions": {
    "target": "es2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "strict": true,
    "outDir": "dist"
  }
}
  • tsconfig.json đặt ở gốc dự án; tsc tự đọc nó khi chạy không kèm tên file.
  • compilerOptions gom mọi tuỳ chọn quan trọng: strict, target, module, outDir...
  • npx tsc --init sinh một bản mẫu đầy đủ chú thích để tham khảo.

strict là tuỳ chọn đáng giá nhất - nó bật cả một nhóm kiểm tra nghiêm. Từ TypeScript 6, strict bật mặc định, và bạn nên giữ vậy. Hai cái nó bắt thường xuyên nhất:

noImplicitAny: tham số quên kiểu

function chao(ten) {        // ten ngam thanh 'any'
  return "Xin chao, " + ten;
}

tsc báo

error TS7006: Parameter 'ten' implicitly has an 'any' type.

strictNullChecks: quên xử lý null

function dodai(s: string | null): number {
  return s.length;          // s co the la null!
}

tsc báo

error TS18047: 's' is possibly 'null'.

Trung thực

Đây là một thay đổi đáng nhớ: các phiên bản TypeScript cũ để strict TẮT mặc định, phải tự bật. TypeScript 6 đã đảo lại - lựa chọn an toàn thành mặc định. Nếu mèo con đọc tài liệu cũ thấy bảo "nhớ bật strict", thì nay nó đã bật sẵn. Tắt nó (strict: false) gần như chỉ để di chuyển dần dự án JS cũ.

Ba tuỳ chọn hay phải đụng khi dựng dự án, và rất dễ lẫn:

  • target: tsc sinh JavaScript theo phiên bản nào (vd es2022) - ảnh hưởng cú pháp trong .js.
  • module: dùng hệ module nào khi sinh code (ESM, commonjs, nodenext...).
  • moduleResolution: tsc TÌM module thế nào (nodenext cho Node hiện đại, bundler cho Vite/webpack).

Mẹo chọn nhanh

Dự án Node hiện đại: "module": "nodenext" + "moduleResolution": "nodenext". Dự án web qua bundler (Vite, webpack): thường "module": "esnext" + "moduleResolution": "bundler". Khi bí, để tsc --init chọn mặc định hợp lý rồi chỉnh sau.

Không phải thư viện JavaScript nào cũng kèm kiểu. Một tệp .d.ts (declaration) chỉ MÔ TẢ kiểu, không chứa code chạy, để gắn kiểu cho phần JavaScript chưa có:

mylib.d.ts

// Mo ta kieu cho mot module JS chua co kieu
declare module "mylib" {
  export function tinhTong(a: number, b: number): number;
}

Sau đó import dùng bình thường, TypeScript đã hiểu kiểu. Nhưng trước khi tự viết, hãy kiểm xem cộng đồng đã làm sẵn chưa - kho DefinitelyTyped cung cấp các gói @types/...:

Cài kiểu cộng đồng

npm install -D @types/node     # kieu cho Node
npm install -D @types/lodash   # kieu cho lodash
  • .d.ts chỉ chứa khai báo kiểu (declare), không có code chạy - gắn kiểu cho JS không sửa nguồn.
  • @types/... là kiểu do cộng đồng viết ở DefinitelyTyped; cài là dùng được ngay.
  • Tự viết .d.ts chỉ khi không có @types sẵn cho thư viện đang dùng.

Một chi tiết nhỏ mà hữu ích: import type chỉ nhập KIỂU, và bị xoá hết khi biên dịch - đúng tinh thần type erasure ở Bài 1, nên không sinh ra import JavaScript thừa lúc chạy:

import type

import type { Meo } from "./meo";   // chi lay kieu, bi xoa khi bien dich
import { taoMeo } from "./meo";     // import gia tri thuong (con lai luc chay)

Bài tiếp theo: Di chuyển JS sang TS

Cấu hình xong, bài sau là phần thực chiến nhất: di chuyển dần một dự án JavaScript sang TypeScript - bật allowJs, dùng JSDoc, siết strict từng bước mà không phải viết lại từ đầu.

Câu hỏi thường gặp

Gần như không bao giờ. Từ TypeScript 6, strict bật MẶC ĐỊNH - đó là một quyết định tốt vì strict chính là phần lớn giá trị của TypeScript. Tắt strict (strict: false) làm TS gần như "mù", bỏ qua null và any ngầm. Chỉ tắt tạm khi đang di chuyển dần một dự án JS lớn (Bài sau).

target nói tsc nên SINH ra JavaScript theo phiên bản nào (es2020, es2022...) - ảnh hưởng cú pháp được dùng trong .js. module nói dùng HỆ MODULE nào (ESM, CommonJS, nodenext...) - ảnh hưởng cách import/export được biên dịch. Hai cái độc lập nhau.

Là các gói khai báo kiểu do cộng đồng viết cho những thư viện JavaScript chưa kèm kiểu, lưu ở kho DefinitelyTyped. Vd cài @types/node là có kiểu cho Node. Trước khi tự viết .d.ts, hãy kiểm xem đã có @types/ten-thu-vien chưa.

Khi dùng một thư viện hoặc module JavaScript chưa có kiểu sẵn lẫn chưa có gói @types. Lúc đó bạn viết một tệp .d.ts mô tả hình dạng của nó để TypeScript hiểu, mà không cần sửa mã nguồn JS gốc.

import type chỉ nhập KIỂU, và bị xoá hoàn toàn khi biên dịch (type erasure, Bài 1) - không sinh ra một dòng import JavaScript nào lúc chạy. Dùng nó khi chỉ cần kiểu từ một module, giúp tránh import thừa và vòng lặp phụ thuộc.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Từ TypeScript 6, strict mode mặc định thế nào?

Bài tập về nhà

  1. 1

    Sinh tsconfig

    Chạy npx tsc --init trong một thư mục trống, mở tsconfig.json sinh ra. Tìm dòng strict và target.

    ✅ Hoàn thành khi: Một tsconfig.json có thật, chỉ ra được dòng strict (bật) và dòng target.

  2. 2

    Bắt no implicit any

    Viết một hàm có tham số KHÔNG chú thích kiểu. Biên dịch với strict (mặc định) xem lỗi.

    ✅ Hoàn thành khi: Ghi lại lỗi TS7006 (implicitly has an any type), và sửa bằng cách thêm kiểu cho tham số.

  3. 3

    Bắt possibly null

    Viết một hàm nhận tham số kiểu string | null rồi truy cập .length ngay. Biên dịch xem lỗi, rồi sửa bằng narrowing.

    ✅ Hoàn thành khi: Ghi lại lỗi TS18047 (possibly null), và bản sửa có kiểm null trước khi dùng.

  4. 4

    Thử tắt strict (rồi bật lại)

    Tạm để strict: false trong tsconfig, xem hai lỗi trên biến mất. Rồi bật lại và đọc kỹ chúng quay về.

    ✅ Hoàn thành khi: Quan sát rõ: tắt strict thì lỗi mất, bật strict thì lỗi quay lại; kèm một câu vì sao nên giữ strict.

  5. 5

    Viết một .d.ts

    Giả sử có một module JS tên "mylib" chưa có kiểu. Viết một mylib.d.ts khai báo một hàm của nó, rồi import dùng trong một file .ts.

    ✅ Hoàn thành khi: File .ts import hàm từ "mylib" và biên dịch sạch nhờ .d.ts; gọi sai kiểu thì tsc báo lỗi.

  6. 6

    import type

    Lấy một chỗ đang import một type từ module khác bằng import thường. Đổi sang import type và biên dịch xem .js sinh ra.

    ✅ Hoàn thành khi: File .js sinh ra KHÔNG còn dòng import cho cái type đó - bằng chứng import type bị xoá.