Chương 12 · TypeScript Compiler, Tooling & Practical

TypeScript trong thực chiến — Compiler, Tooling và Patterns

Sau khi đã quen với syntax, type system và advanced types, đây là chương "đưa TypeScript vào dự án thật". Bạn sẽ master tsconfig.json, hiểu sâu module resolution, viết được .d.ts declaration cho thư viện JS thuần, dựng project references monorepo, và biết khi nào dùng tsx, tsc, tsup, hay Vite. Đây cũng là chương tổng kết sub-pillar JavaScript.

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 9-11
🎯 Mục tiêu chương
  • Đọc và viết được tsconfig.json đầy đủ — hiểu rõ compilerOptions, include, exclude, references.
  • Chọn đúng target / module / moduleResolution cho 3 ngữ cảnh: Node CLI, SPA browser, library publish.
  • Hiểu sâu strict mode — biết "strict": true bật bao nhiêu flag và mỗi flag bảo vệ gì.
  • Viết được file .d.ts đơn giản cho thư viện không có type.
  • Khai báo ambient module và mở rộng Window, NodeJS.ProcessEnv, ...
  • Dựng monorepo với project references — incremental build, isolate type-check.
  • Master pattern thực tế: typed env (zod), typed fetch wrapper, repository pattern.
  • Debug TypeScript với source map và VS Code.
🧠 Mental model — TypeScript là một build tool

Khác với Java/C# compile sang bytecode chạy thẳng, TypeScript chỉ làm 2 việc: (1) kiểm tra type, (2) strip annotation type ra để xuất JavaScript thuần. Output đó mới được Node/browser chạy. Vì vậy mọi đặc tả runtime (target ES version, module format, paths…) phải khớp với môi trường chạy. Chương này dạy bạn cách khớp đúng.

1. tsconfig.json — giải phẫu file config

tsconfig.json là file JSON đặt ở root dự án. Khi gõ tsc không tham số, compiler tự tìm tsconfig.json ở thư mục hiện tại (rồi đi lên cha) và đọc config từ đó.

4 phần chính:

FieldMục đíchBắt buộc
compilerOptionsCấu hình compiler: target, module, strict, paths…Có (gần như)
includeGlob các file/thư mục cần compileKhông (default tất cả .ts)
excludeLoại trừ file khỏi compile (mặc định: node_modules, bower_components, jspm_packages, outDir)Không
referencesLiên kết tới các sub-project (monorepo)Không
extendsKế thừa từ tsconfig khác (vd: @tsconfig/node20)Không
filesLiệt kê file cụ thể (thay vì glob)Không

Một tsconfig.json tối thiểu cho project mới:

tsconfig.json — minimal
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}
💡 Khởi tạo nhanh

Chạy npx tsc --init để TypeScript sinh ra tsconfig.json mặc định có comment giải thích từng option. Tốt cho người mới đọc trước, sau đó xoá các option không dùng.

Hoặc kế thừa template chính thức: npm i -D @tsconfig/node20 rồi "extends": "@tsconfig/node20/tsconfig.json".

2. targetmodule — chọn đúng phiên bản

Hai option quan trọng nhất, hay bị nhầm. target = phiên bản JavaScript output; module = format module (CommonJS, ESM, …).

2.1. target — phiên bản ECMAScript output

TypeScript sẽ down-level các tính năng JS hiện đại về phiên bản này. Ví dụ target: "ES5" khiến optional chaining ?. được biên dịch thành chuỗi &&.

targetKhi nào dùng
ES5Hỗ trợ IE11 (đã chết). Tránh.
ES2015Browser cũ. Hiếm dùng 2026.
ES2020Node 14+, browser modern. Có optional chaining, nullish coalescing.
ES2022Sweet spot 2026. Có class fields, top-level await, at().
ESNextBleeding edge. Output không down-level. Phù hợp library bundle bởi consumer.

2.2. module — định dạng module output

moduleOutput dạngKhi dùng
CommonJSmodule.exports + require()Node legacy, package CJS
ESNextimport / export giữ nguyênBundler (Vite, Webpack) tự xử lý
NodeNextMix CJS/ESM theo file extensionNode ESM thuần (package.json "type":"module")
UMD / AMD / SystemJSFormat legacyHầu như không dùng nữa
🔥 Gotcha — target và module phải khớp runtime

Đặt target: "ES2022" nhưng triển khai code lên Node 12 → top-level await không có, crash. Đặt module: "CommonJS" nhưng dùng import.meta.url → lỗi. Luôn check matrix: target ↔ runtime version ↔ module format ↔ package.json "type".

3. Strict mode — luôn bật

Cờ "strict": true bật 8+ flag con cùng lúc. Đây là cách TypeScript thực sự bảo vệ bạn khỏi bug. Luôn bật strict cho project mới. Cho project cũ migrate dần thì bật từng flag.

FlagTác dụng
strictNullChecksPhân biệt null/undefined với type khác. Quan trọng nhất.
noImplicitAnyBắt buộc khai báo type, không cho ngầm any.
strictFunctionTypesCheck contravariant tham số function chặt chẽ.
strictBindCallApply.bind/.call/.apply check type tham số.
strictPropertyInitializationClass property phải init trong constructor hoặc khai báo.
alwaysStrictOutput có 'use strict' (parse module ở strict mode).
noImplicitThisCấm this kiểu any.
useUnknownInCatchVariablescatch (e) ngầm unknown, không phải any.

Các flag bổ sung khuyên bật thêm (không trong strict):

tsconfig — strict++
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,   // arr[i] có thể undefined
    "noImplicitOverride": true,        // override method phải có keyword override
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true    // { x?: number } khác { x: number | undefined }
  }
}

4. Module resolution — cách TS tìm file

Khi bạn viết import x from './foo' hay import lodash from 'lodash', TypeScript phải tìm file thực tương ứng. Cách tìm phụ thuộc moduleResolution:

moduleResolutionHành viKhi dùng
classicLegacy TS 1.x. Đừng dùng.
nodeBắt chước Node CommonJS: ./foo./foo.ts, ./foo/index.ts, …Node CJS classic, project cũ.
node16 / nodenextNode ESM: bắt buộc .js trong import (cả khi file là .ts!)Node ESM với "type":"module".
bundlerBundler-friendly: không cần extension, hỗ trợ paths, condition exports.Mặc định cho Vite/Webpack/esbuild.
🔥 Gotcha — phải có .js trong NodeNext

Với moduleResolution: "nodenext", dù file của bạn là foo.ts, bạn vẫn phải import bằng import { x } from './foo.js'. Lý do: sau khi compile ra foo.js, Node ESM yêu cầu extension đầy đủ. TypeScript "nói trước" để code output runnable.

5. Output options — controls cho file sinh ra

OptionMục đích
outDirThư mục chứa output. Mặc định cùng folder với file .ts.
rootDirThư mục gốc của source. Cấu trúc trong rootDir được giữ ở outDir.
declarationSinh kèm file .d.ts. Bắt buộc nếu publish library.
declarationMapSinh .d.ts.map để "Go to Definition" nhảy về file .ts gốc.
sourceMapSinh .js.map — debug breakpoint trên .ts.
removeCommentsLoại bỏ comment trong output.
importHelpersDùng tslib để chia sẻ helper (giảm size khi nhiều file).
noEmitChỉ type-check, không xuất file. Hữu ích khi đã có Vite/esbuild compile.
incrementalLưu cache .tsbuildinfo, build lần sau nhanh hơn.

6. Path mapping — import alias đẹp

Thay vì import Button from '../../../components/Button', ta muốn import Button from '@/components/Button'. Cấu hình:

tsconfig.json — paths
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*":        ["src/*"],
      "@utils/*":   ["src/utils/*"],
      "@types":     ["src/types/index.ts"]
    }
  }
}

Code sau khi cấu hình:

// trước
import Button from '../../../components/Button';

// sau
import Button from '@/components/Button';
🔥 Quan trọng — paths chỉ là "trick type-check"

tsc hiểu path mapping khi type-check, nhưng không rewrite import lúc emit. Khi runtime, Node sẽ vẫn cố tìm package tên "@". Bạn cần một trong các giải pháp:

  • Vite: cấu hình thêm resolve.alias trong vite.config.ts (Vite có plugin vite-tsconfig-paths đọc tự động).
  • esbuild/tsup: dùng plugin alias hoặc esbuild-plugin-tsconfig-paths.
  • Node thuần (tsx/ts-node): tsx hỗ trợ tự động; với ts-node cần tsconfig-paths/register.

7. .d.ts — file khai báo type

File .d.ts chứa chỉ khai báo type, không có code chạy. Mục đích: mô tả "shape" của 1 module JS thuần để TS hiểu type khi import.

3 nguồn .d.ts:

  1. Đi kèm package: nhiều thư viện modern (zod, react, vue) ship sẵn file .d.ts trong npm package.
  2. DefinitelyTyped — kho cộng đồng. Cài qua npm i -D @types/lodash, @types/node, @types/express, …
  3. Tự viết — khi không có 2 nguồn trên.

Ví dụ .d.ts tự viết cho thư viện hypothesi string-strip-html:

types/string-strip-html.d.ts
declare module 'string-strip-html' {
  export function stripHtml(
    html: string,
    opts?: { onlyStripTags?: string[] }
  ): { result: string };
}
💡 Đặt file .d.ts ở đâu?

Tạo thư mục src/types/ hoặc types/ ở root. Đảm bảo nằm trong include của tsconfig.json (hoặc khai báo thêm "typeRoots"). TypeScript sẽ tự load các file .d.ts ở đó.

8. Ambient declaration — khai báo toàn cục

Mở rộng kiểu của thứ "có sẵn" như Window, globalThis, hay process.env.

8.1. Mở rộng Window / globalThis

src/types/global.d.ts
declare global {
  interface Window {
    myAnalytics: {
      track(event: string): void;
    };
  }
}

// Cần dòng export để file thành module — declare global mới hiệu lực
export {};

Lưu ý dòng export {}; ở cuối — nếu thiếu, file bị coi là script global, declare global không hoạt động.

8.2. Type hoá process.env

src/types/env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      DATABASE_URL: string;
      PORT?: string;
    }
  }
}

export {};

Cách này khai man — chỉ tell TS, không validate runtime. Pattern an toàn hơn: dùng zod validate process.env rồi infer type (xem §13).

8.3. declare module cho CSS, ảnh, file lạ

src/types/assets.d.ts
declare module '*.css';
declare module '*.svg' {
  const src: string;
  export default src;
}
declare module '*.md?raw' {
  const content: string;
  export default content;
}

9. Project references — Monorepo nhiều package

Khi dự án có nhiều package cùng repo (vd: packages/utils, packages/app), bạn không muốn 1 file tsconfig.json khổng lồ. Mỗi package có tsconfig.json riêng, link với nhau qua references.

Cấu trúc thư mục mẫu:

monorepo/
├─ tsconfig.json                // solution-style root: chỉ liệt kê references
├─ packages/
│  ├─ utils/
│  │  ├─ tsconfig.json
│  │  └─ src/index.ts
│  └─ app/
│     ├─ tsconfig.json
│     └─ src/index.ts
└─ package.json

Root tsconfig.json:

{
  "files": [],
  "references": [
    { "path": "./packages/utils" },
    { "path": "./packages/app" }
  ]
}

packages/utils/tsconfig.json:

{
  "compilerOptions": {
    "composite": true,           // bắt buộc khi được reference
    "declaration": true,
    "outDir": "dist",
    "rootDir": "src",
    "target": "ES2022",
    "module": "ESNext",
    "strict": true
  },
  "include": ["src"]
}

packages/app/tsconfig.json:

{
  "extends": "../utils/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "references": [{ "path": "../utils" }]
}

Build:

$ tsc -b              # build tất cả references theo thứ tự
$ tsc -b --clean      # xoá tất cả output
$ tsc -b --watch      # incremental + watch
💡 Lợi ích project references
  • Incremental build: thay đổi utils chỉ rebuild utils + downstream phụ thuộc, không rebuild toàn bộ.
  • Isolate: package có config riêng (vd target khác).
  • Type-check song song trên CI.

10. Setup case 1 — Node CLI

Mục tiêu: viết script Node bằng TypeScript, chạy nhanh trong dev, build sang JS để deploy.

Bước 1: khởi tạo

$ mkdir my-cli && cd my-cli
$ npm init -y
$ npm i -D typescript tsx @types/node
$ npx tsc --init

Bước 2: tsconfig.json

tsconfig.json — Node CLI
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": false,
    "sourceMap": true
  },
  "include": ["src"]
}

Trong package.json thêm "type": "module" và scripts:

{
  "type": "module",
  "scripts": {
    "dev":   "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "build": "tsc",
    "check": "tsc --noEmit"
  }
}

Bước 3: viết src/index.ts

import { readdir } from 'node:fs/promises';

async function main(): Promise<void> {
  const dir = process.argv[2] ?? '.';
  const entries = await readdir(dir);
  console.log(`${entries.length} file/folder trong ${dir}`);
}

main();

Chạy dev: npm run dev. Build deploy: npm run buildnode dist/index.js.

11. Setup case 2 — SPA browser với Vite

Vite tự xử lý TS — không cần chạy tsc trong dev (chỉ tsc --noEmit để type-check).

$ npm create vite@latest my-app -- --template vanilla-ts
$ cd my-app && npm install
$ npm run dev

tsconfig.json Vite sinh ra (đơn giản hoá):

tsconfig.json — Vite SPA
{
  "compilerOptions": {
    "target": "ES2022",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noEmit": true,
    "isolatedModules": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src"]
}

Để Vite resolve @/*:

vite.config.ts
import { defineConfig } from 'vite';
import path from 'node:path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
});

Hoặc nhanh hơn: cài vite-tsconfig-paths, plugin tự đọc paths trong tsconfig.json.

12. Setup case 3 — Publish library

Library cần xuất 3 thứ: CJS (cho Node CJS cũ), ESM (cho Node modern + bundler), .d.ts (cho TS user). Công cụ đề xuất: tsup (wrap esbuild, zero-config).

$ npm i -D tsup typescript

tsup.config.ts:

import { defineConfig } from 'tsup';

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['cjs', 'esm'],
  dts: true,
  sourcemap: true,
  clean: true,
  target: 'es2022'
});

package.json với exports map:

{
  "name": "my-lib",
  "version": "1.0.0",
  "main":    "./dist/index.cjs",
  "module":  "./dist/index.js",
  "types":   "./dist/index.d.ts",
  "exports": {
    ".": {
      "types":   "./dist/index.d.ts",
      "import":  "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "check": "tsc --noEmit"
  }
}

Build: npm run build. Publish: npm publish. User cài về sẽ thấy autocomplete đầy đủ.

13. Patterns thực tế

13.1. Typed env với zod

Validate process.env ngay khi app start. Sai → throw sớm, đỡ debug runtime mơ hồ.

src/env.ts
import { z } from 'zod';

const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  PORT: z.coerce.number().int().positive().default(3000)
});

export const env = EnvSchema.parse(process.env);
// env có type: { NODE_ENV: 'development' | ..., DATABASE_URL: string, PORT: number }

13.2. Typed fetch wrapper

src/api.ts
export class ApiError extends Error {
  constructor(public readonly status: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

export async function api<T>(
  url: string,
  init?: RequestInit
): Promise<T> {
  const res = await fetch(url, init);
  if (!res.ok) {
    throw new ApiError(res.status, `HTTP ${res.status}`);
  }
  return res.json() as Promise<T>;
}

// Usage:
interface User { id: number; name: string; }
const u = await api<User>('/api/users/1');
// u tự động có type User

Cảnh báo: as Promise<T> là "tin tưởng server trả đúng type". Production nên kết hợp zod: UserSchema.parse(await res.json()) — runtime validation luôn.

13.3. Repository pattern

src/repository/user.ts
export interface User { id: number; email: string; }

export interface UserRepository {
  findById(id: number): Promise<User | null>;
  create(data: Omit<User, 'id'>): Promise<User>;
}

// Triển khai 1: Sqlite
export class SqliteUserRepo implements UserRepository {
  async findById(id: number) { /* ... */ return null; }
  async create(data: Omit<User, 'id'>) { /* ... */ return { id: 1, ...data }; }
}

// Triển khai 2: In-memory (cho test)
export class MockUserRepo implements UserRepository {
  private users = new Map<number, User>();
  async findById(id: number) { return this.users.get(id) ?? null; }
  async create(data: Omit<User, 'id'>) {
    const id = this.users.size + 1;
    const user = { id, ...data };
    this.users.set(id, user);
    return user;
  }
}

Caller chỉ phụ thuộc vào UserRepository interface — đổi triển khai mà không phải sửa logic. Đây là Dependency Inversion Principle (Pillar OOP) áp dụng vào TypeScript thực tế.

14. Debug TypeScript

3 cách phổ biến:

  1. console.log ngon. Với tsx, log hiện ngay file .ts gốc (nhờ source map nội bộ).
  2. VS Code debugger với source map. Bật "sourceMap": true trong tsconfig.json, tạo launch config:
.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TS (tsx)",
      "runtimeExecutable": "tsx",
      "args": ["${workspaceFolder}/src/index.ts"],
      "skipFiles": ["<node_internals>/**"]
    }
  ]
}

Đặt breakpoint trực tiếp trên file .ts, F5 chạy. Variable inspector hiện đúng tên biến TS.

  1. Chrome DevTools cho browser: Vite tự bật source map dev. Mở DevTools → tab Sources → tìm file .ts trong tree, đặt breakpoint như JS thường.

Bài tập

Bài 1 — TS Node CLI từ đầu

Tạo project Node CLI mới. Cài tsx + typescript + vitest.

  • Viết src/count-files.ts nhận đường dẫn từ process.argv[2], dùng node:fs/promises đếm file (không tính folder con).
  • Script npm run dev dùng tsx chạy trực tiếp.
  • Script npm run build compile tsc ra dist/. Verify chạy được bằng node dist/count-files.js ..
  • Viết test vitest đơn giản: tạo folder tạm, ghi 3 file, gọi hàm countFiles, expect 3.
Gợi ý cấu trúc
// src/count-files.ts
import { readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';

export async function countFiles(dir: string): Promise<number> {
  const entries = await readdir(dir);
  let count = 0;
  for (const e of entries) {
    const s = await stat(join(dir, e));
    if (s.isFile()) count++;
  }
  return count;
}

if (import.meta.url === `file://${process.argv[1]}`) {
  const dir = process.argv[2] ?? '.';
  countFiles(dir).then(n => console.log(n));
}

Bài 2 — Vite + path alias

Khởi tạo Vite + framework yêu thích (vanilla-ts / vue-ts / svelte-ts / react-ts). Sửa tsconfig.json:

  • Bật "strict": true + "noUncheckedIndexedAccess": true.
  • Thêm "baseUrl": ".""paths": { "@/*": ["src/*"] }.
  • Cập nhật vite.config.ts với resolve.alias tương ứng (hoặc cài vite-tsconfig-paths).
  • Tạo src/components/Hello.ts export 1 function. Trong src/main.ts import bằng @/components/Hello. Verify hot reload hoạt động.
Tips

Nếu hot-reload báo lỗi "Cannot find module '@/components/Hello'", có thể bạn quên config alias bên Vite — TS only nhận paths cho type-check, runtime cần Vite biết alias.

Bài 3 — Tự viết file .d.ts

Cài 1 npm package không có type. Ví dụ phiên bản cũ của string-strip-html@4 hoặc 1 package nhỏ tự chọn. Khi import, TS sẽ báo "Could not find a declaration file".

  • Tạo src/types/<ten-pkg>.d.ts.
  • Viết declare module '...' với type tối thiểu (signature 1-2 hàm bạn thật sự dùng).
  • Verify autocomplete hoạt động trong VS Code.
  • Đảm bảo file .d.ts nằm trong include của tsconfig.json.
Mẫu
declare module 'string-strip-html' {
  export function stripHtml(
    html: string
  ): { result: string };
}

Bài 4 — Typed fetch wrapper

Implement hàm api<T>(url, init?):

  • Generic T cho shape data trả về.
  • Throw ApiError nếu !res.ok, chứa status, message.
  • Test với https://jsonplaceholder.typicode.com/users/1. Khai báo interface User, gọi api<User>(...), in tên.
  • Bonus: thêm option timeout (dùng AbortController).
  • Bonus 2: kết hợp zod để runtime-validate response.
Skeleton
export class ApiError extends Error {
  constructor(public status: number, msg: string) { super(msg); }
}

export async function api<T>(
  url: string,
  init: RequestInit & { timeoutMs?: number } = {}
): Promise<T> {
  const { timeoutMs = 10000, ...rest } = init;
  const ctrl = new AbortController();
  const t = setTimeout(() => ctrl.abort(), timeoutMs);
  try {
    const res = await fetch(url, { ...rest, signal: ctrl.signal });
    if (!res.ok) throw new ApiError(res.status, res.statusText);
    return res.json() as Promise<T>;
  } finally {
    clearTimeout(t);
  }
}

Bài 5 — Mini monorepo với project references

Tạo cấu trúc:

my-mono/
├─ tsconfig.json
├─ packages/
│  ├─ utils/  (export function add)
│  └─ app/    (import add từ utils, gọi log)
  • Mỗi package có tsconfig.json riêng, composite: true ở utils.
  • Root tsconfig.json chỉ chứa references.
  • app/tsconfig.jsonreferences trỏ tới ../utils.
  • Build bằng tsc -b. Kiểm tra .tsbuildinfo được tạo.
  • Edit utils/src/index.ts → rebuild → confirm chỉ utilsapp bị rebuild (chứ không phải toàn bộ).
Tips

Import bên app: import { add } from '../../utils/src/index' sẽ chạy, nhưng xấu. Cách "đẹp": dùng workspace của npm/pnpm/yarn, rồi import bằng tên package (import { add } from '@my-mono/utils'). Đây là bước nâng cao — không bắt buộc trong bài này.

Quiz

Q1

"strict": true trong tsconfig bật những flag con nào?

Xem đáp án
✓ Đáp án

8 flag: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, noImplicitThis, useUnknownInCatchVariables. Đây không phải set cố định mãi — TypeScript có thể thêm flag mới vào strict ở phiên bản sau (vd useUnknownInCatchVariables được thêm ở TS 4.4).

Q2

Khác nhau giữa tsxts-node? Nên dùng cái nào?

Xem đáp án
✓ Đáp án

tsx dùng esbuild bên dưới — chạy rất nhanh nhưng chỉ strip type, không type-check. ts-node gọi tsc thật — chậm hơn nhưng type-check khi chạy.

Khuyến nghị 2026: tsx cho dev (nhanh), tsc --noEmit riêng để type-check trên CI hoặc pre-commit. ts-node hiện vẫn dùng nhưng không còn là default.

Q3

"moduleResolution": "bundler" dùng khi nào?

Xem đáp án
✓ Đáp án

Khi dự án của bạn được biên dịch/bundle bởi bundler hiện đại (Vite, esbuild, Webpack, Rollup) chứ không phải Node ESM thuần. Bundler tự xử lý extension .js, conditional exports, path mapping… nên bundler cho phép import "loose": import x from './foo' không cần .js.

Khi nào không dùng? Code chạy thẳng Node ESM (không qua bundler) → dùng nodenext để TS bắt bạn viết extension đầy đủ.

Q4

File .d.ts có chứa logic JavaScript không?

Xem đáp án
✓ Đáp án

Không. File .d.ts chỉ chứa khai báo: declare, interface, type, export signature… Không có thân hàm, không có console.log, không có code chạy.

Nếu thử viết export function foo() { return 1; } trong .d.ts, TS sẽ báo lỗi "An implementation cannot be declared in ambient contexts".

Q5

Mặc định tsconfig.json không có "include" thì TS compile file nào?

Xem đáp án
✓ Đáp án

Tất cả file .ts, .tsx, .d.ts (và .js/.jsx nếu bật allowJs) trong thư mục chứa tsconfig.json và mọi thư mục con — trừ những cái khớp exclude (mặc định node_modules, bower_components, jspm_packages, và outDir).

Q6

Vì sao project Node luôn cần cài @types/node?

Xem đáp án
✓ Đáp án

Node.js là binary C++, không có source TypeScript. Các API built-in (fs, path, http, process, Buffer…) cần khai báo type bổ sung từ ngoài. Cộng đồng cung cấp qua DefinitelyTyped, publish thành package @types/node. Cài về và TS tự dùng (do typeRoots mặc định bao gồm node_modules/@types).

Q7

Project references giải quyết vấn đề gì?

Xem đáp án
✓ Đáp án

3 vấn đề trong monorepo nhiều package:

  1. Incremental build: tsc -b chỉ rebuild package đã đổi và downstream phụ thuộc, không scan lại toàn bộ.
  2. Type-check song song: mỗi package isolate, có thể chạy song song trên CI.
  3. Config riêng: mỗi package có target/lib/strict riêng nếu cần (vd: package frontend cần DOM, package CLI không cần).

Cần đặt "composite": true ở package được reference. Build qua tsc -b thay vì tsc thường.

Tổng kết

Sau chương 12 bạn nên đã master:

  • tsconfig.json anatomy — 4 phần chính, biết cấu hình cho từng loại dự án.
  • target / module / moduleResolution — chọn đúng matrix khớp runtime.
  • Strict mode — bật 8+ flag, biết mỗi flag bảo vệ gì.
  • Path mapping@/* alias, nhớ cấu hình thêm phía bundler.
  • .d.ts — đọc, viết, đặt đúng vị trí. declare moduledeclare global.
  • Project references — monorepo nhiều package, build incremental.
  • 3 setup cases — Node CLI (tsx + tsc), Vite SPA, Library publish (tsup).
  • Patterns thực tế — zod env, typed fetch, repository pattern.
  • Debug TS — source map + VS Code launch config.

Kết nối

  • Chương 9 (TS Types) — các type primitive và composite bạn đang viết trong .d.ts.
  • Chương 10 (TS Generics) — generic api<T>, UserRepository dùng trong patterns ở §13.
  • Chương 11 (TS Advanced) — utility types, mapped types, conditional types tỏ ra hữu dụng khi viết .d.ts.
  • Chương 8 (Modules) — module resolution chính là cầu nối: TS map import path → file thực, hệt như Node module resolution.
  • Pillar OOP — Dependency Inversion — repository pattern ở §13.3 là DIP trực tiếp.
  • Dart Phase 2 — Dart không có tsconfig.json; pubspec.yaml tương đương package.json + một phần tsconfig.json (target SDK), nhưng Dart compile native nên không có concept "moduleResolution".
🎉 Chúc mừng — bạn đã hoàn thành sub-pillar JavaScript!

12 chương, ~10.000 dòng kiến thức. Bạn đã đi từ "Hello JavaScript" qua biến/coercion, functions/closures, objects/prototype, arrays/iterables, async/await, error handling, modules, đến TypeScript đầy đủ với compiler và tooling thực chiến. Đây là nền tảng đủ mạnh để bạn:

  • Đọc hiểu mọi codebase JS/TS modern (Next.js, NestJS, Vue, Svelte, …).
  • Đóng góp pull request vào dự án open-source TypeScript.
  • Phỏng vấn vị trí Frontend / Fullstack / Node backend tự tin.
  • Tiếp tục học framework cụ thể mà không bị tắc ở "syntax JS lạ".

Bước tiếp theo đề xuất: chuyển sang sub-pillar Python — ngôn ngữ thứ hai cực kỳ giá trị (data, scripting, AI/ML, backend). Học song song JS/TS ↔ Python sẽ giúp bạn thấy rõ trade-off design của từng ngôn ngữ.

Hoặc tiếp tục đào sâu hệ JS: học framework cụ thể (Next.js, NestJS), thư viện type-heavy như tRPC, Effect, hoặc đọc source code TypeScript compiler để hiểu type-checking phía sau.