Chương 09 · TypeScript Type System Foundations

TypeScript — nền tảng hệ thống kiểu

TypeScript là superset của JavaScript — mọi JS hợp lệ đều là TS hợp lệ. Khác biệt: TS thêm type annotation tồn tại lúc biên dịch rồi biến mất khi chạy. Nó không sửa JS, không thay đổi runtime — nó chỉ chặn bug trước khi bạn deploy. "JavaScript that scales."

Độ dài: ~1000 dòng Bài tập: 5 Quiz: 7 Prerequisites: Chương 1-8 (JS core)
🎯 Mục tiêu chương
  • Hiểu TS là gì, vì sao "type tan biến lúc runtime" và tại sao điều đó vẫn cực kỳ giá trị.
  • Setup project TS từ con số 0: typescript, tsx, tsconfig.json.
  • Master các primitive type và các type đặc biệt: any, unknown, never, void.
  • Sử dụng literal type + union để mô hình hoá trạng thái — pattern thay thế enum.
  • Phân biệt type alias và interface, biết khi nào chọn cái nào.
  • Hiểu type narrowing: typeof, instanceof, in, type predicate.
  • Dùng type assertion (as) đúng chỗ, không lạm dụng.

1. TypeScript là gì?

TypeScript được Microsoft phát hành năm 2012, ngày nay đã thành de facto standard cho mọi dự án JavaScript trung bình trở lên. Định nghĩa ngắn nhất:

🧠 Định nghĩa

TypeScript = JavaScript + Static Type System. Một superset: mọi file .js hợp lệ đổi đuôi sang .ts vẫn chạy được. Compiler tsc đọc .ts, kiểm tra type, rồi xoá toàn bộ annotation để xuất ra .js tinh khiết.

Ba ý quan trọng cần nhớ ngay từ chương đầu của TS:

  1. Type chỉ tồn tại lúc compile. Khi code đã build ra .js, không còn dấu vết : string, : number nào. Runtime vẫn là JS thuần — vẫn có typeof, vẫn có coercion, vẫn có NaN (chương 2).
  2. TS không sửa lỗi của JS. Nó chỉ báo trước những lỗi mà JS sẽ silently nuốt. 0.1 + 0.2 !== 0.3 vẫn đúng trong TS.
  3. TS là tooling layer. IDE auto-complete, refactor an toàn, đọc code lạ dễ hơn — đó mới là 85% giá trị của TS, không phải "an toàn".

So sánh nhanh JS vs TS:

JS — dynamic
// JavaScript: ai cũng gọi được, lỗi runtime
function greet(name) {
  return 'Xin chào, ' + name.toUpperCase();
}

greet('An');     // 'Xin chào, AN'
greet(42);       // 💥 TypeError: name.toUpperCase is not a function
greet();         // 💥 TypeError: Cannot read properties of undefined
TS — static check
// TypeScript: lỗi BẮT được trước khi chạy
function greet(name: string): string {
  return 'Xin chào, ' + name.toUpperCase();
}

greet('An');     // ✅ ok
greet(42);       // ❌ TS2345: Argument of type 'number' not assignable to 'string'
greet();         // ❌ TS2554: Expected 1 arguments, but got 0
💡 Slogan của TS team

"JavaScript that scales." — Khi codebase còn 200 dòng, JS đủ. Khi nó lên 20k dòng, 5 dev, deploy mỗi ngày, type system trở thành thứ giữ cho refactor không thành ác mộng.

2. Setup project TypeScript

Có nhiều cách setup TS. Cách hiện đại nhất 2026 cho project nhỏ-vừa: dùng tsx chạy trực tiếp, tsc --noEmit để kiểm tra type khi cần build kiểm soát chất lượng.

Setup tối thiểu
# 1. Khởi tạo project Node
npm init -y

# 2. Cài TypeScript + tsx (chạy .ts trực tiếp, không build)
npm install -D typescript tsx @types/node

# 3. Sinh tsconfig.json
npx tsc --init

# 4. Tạo file hello.ts
echo 'const msg: string = "Hello TS"; console.log(msg);' > hello.ts

# 5. Chạy thử
npx tsx hello.ts        # Hello TS — không cần compile trước

# 6. Kiểm tra type (không build)
npx tsc --noEmit        # Báo lỗi type nếu có

# 7. Build thật ra .js (nếu cần deploy)
npx tsc                 # Sinh hello.js theo cấu hình tsconfig

3 công cụ quan trọng cần phân biệt:

Công cụVai tròKhi dùng
tscCompiler chính thức. Đọc .ts → kiểm type → xuất .js.Build production, --noEmit để CI check type.
tsxWrapper trên esbuild — chạy .ts trực tiếp, không xuất file. Không type check.Dev/script, chạy file nhanh.
ts-nodeWrapper trên tsc — chạy .ts + type check (chậm hơn).Cũ, hiện ít dùng. tsx thay thế.
🔥 Gotcha — tsx không báo lỗi type

tsx tối ưu cho tốc độ: nó dùng esbuild chỉ strip annotation, không kiểm tra. Đoạn code có lỗi type vẫn chạy bình thường. Đừng nhầm tsx run ok với tsc check ok.

Workflow đúng: code → chạy bằng tsx để xem output → chạy tsc --noEmit (hoặc plugin IDE) để bắt lỗi type → commit.

3. tsconfig.json — cấu hình compiler

File tsconfig.json là "linh hồn" của một project TS — quyết định compiler nghiêm tới mức nào, chuyển output ra phiên bản JS nào, hỗ trợ JSX/React không.

tsconfig.json tối thiểu cho 2026
{
  "compilerOptions": {
    "target": "ES2022",           // Output JS phiên bản nào
    "module": "NodeNext",         // Module system (Node hiện đại)
    "moduleResolution": "NodeNext",

    "strict": true,                // 🎯 Bật tất cả strict flags
    "noUncheckedIndexedAccess": true, // arr[0] có thể undefined
    "noImplicitOverride": true,

    "esModuleInterop": true,       // Cho import default từ CommonJS
    "skipLibCheck": true,          // Bỏ qua type check trong node_modules
    "forceConsistentCasingInFileNames": true,

    "outDir": "./dist",             // Nơi xuất .js
    "rootDir": "./src",            // Gốc source .ts
    "declaration": true,           // Xuất kèm .d.ts
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Các option quan trọng nhất:

OptionÝ nghĩaGiá trị nên dùng
targetJS phiên bản output (ES5, ES2015, …, ESNext).ES2022 cho Node hiện đại / browser ≥2023.
moduleModule system output: CommonJS, ESNext, NodeNext.NodeNext cho server, ESNext cho bundler.
strictBật cùng lúc 8 flags strict (noImplicitAny, strictNullChecks, …).Luôn bật true.
esModuleInteropHỗ trợ import x from 'mod' với module CommonJS.true.
skipLibCheckBỏ qua kiểm tra type trong node_modules.true (tăng tốc 5-10×).
outDirThư mục xuất .js."./dist".
noUncheckedIndexedAccessTruy cập arr[i] trả về T | undefined.true (cứng hơn, ít bug hơn).
💡 Luôn bật strict: true

Khởi tạo bằng tsc --init sẽ tự động set strict: true. Đừng tắt. Cờ này gồm strictNullChecks (phân biệt T với T | null), noImplicitAny (cấm implicit any) — đây là 80% sức mạnh thực tế của TS.

4. Primitive types và inference

TS có đầy đủ primitive như JS (chương 2): string, number, boolean, bigint, symbol, null, undefined. Cách annotate đơn giản dùng dấu hai chấm sau tên biến.

Annotation cơ bản
let name: string = 'Việt';
let age: number = 22;
let active: boolean = true;
let big: bigint = 100n;
let uid: symbol = Symbol('id');
let nothing: null = null;
let notSet: undefined = undefined;

// Array
let nums: number[] = [1, 2, 3];
let names: Array<string> = ['a', 'b'];   // equivalent cú pháp generic

// Tuple — fixed length, fixed type per slot
let pair: [string, number] = ['age', 22];

Nhưng thường không cần annotate. TS có khả năng inference rất mạnh: nó tự suy ra type từ giá trị khởi tạo.

Inference vs annotation
// Verbose — không cần
let a: string = 'hello';

// Inference — TS tự biết a là string
let a = 'hello';            // inferred: string
const b = 'hello';          // inferred: 'hello' (literal!)

// Sau đó vẫn type-safe
a = 42;  // ❌ TS2322: Type 'number' not assignable to 'string'
💡 Quy tắc annotation
  • Tin vào inference cho biến local có khởi tạo.
  • Annotate ở: parameter của function, return type (khi muốn ép), object shape lớn, biến chưa khởi tạo.
  • Lý do: code thêm chữ vô ích → khó đọc; nhưng public API cần annotate để contract rõ ràng.

5. Các type đặc biệt: any, unknown, never, void

4 type "siêu nhân" của TS. Phân biệt chúng là test quan trọng nhất để biết bạn hiểu TS hay chưa.

TypeÝ nghĩaCó thể assign vào?Có thể gọi method?
any"Tắt type check cho biến này"Mọi giá trịMọi method (không check)
unknown"Tôi chưa biết, hãy narrow trước"Mọi giá trịKhông gọi được gì cho đến khi narrow
never"Không bao giờ có giá trị"Không thể (trừ never)Không gọi được
void"Function không return gì"Chỉ undefinedKhông gọi được

any — backdoor nguy hiểm

any — tắt mọi check
let x: any = 'hello';
x = 42;                  // ok
x = { a: 1 };             // ok
x.toUpperCase();         // ok (TS không check, runtime sẽ 💥 vì 42 không có method này)
x.whatever().chain();   // ok ở compile, 💥 ở runtime

anybackdoor bỏ qua hệ thống type. Một biến any có thể "lây lan" sang biến khác (assign sang biến string được luôn). Hậu quả: project mất type-safety dần. Coi any như "TODO: chưa nghĩ ra type" và luôn tìm cách thay thế.

unknown — phiên bản an toàn của any

unknown — phải narrow trước
let x: unknown = JSON.parse('{"a":1}');

x.toUpperCase();         // ❌ Object is of type 'unknown'
x.a;                     // ❌ cùng lỗi

// Phải narrow trước
if (typeof x === 'string') {
  x.toUpperCase();       // ✅ x bây giờ là string
}

if (typeof x === 'object' && x !== null && 'a' in x) {
  console.log(x.a);     // ✅
}

unknown"any nhưng văn minh". Bạn không thể làm gì với nó cho đến khi chứng minh nó là kiểu cụ thể. Dùng cho input không tin tưởng được: JSON.parse, response API, dữ liệu từ localStorage, error trong catch.

never — không bao giờ xảy ra

never — bottom type
// Function throw — không bao giờ return bình thường
function fail(msg: string): never {
  throw new Error(msg);
}

// Function vòng lặp vô hạn
function loop(): never {
  while (true) { }
}

// Exhaustiveness check — phổ biến nhất
type Shape = { kind: 'circle' } | { kind: 'square' };

function area(s: Shape) {
  switch (s.kind) {
    case 'circle': return 3.14;
    case 'square': return 1;
    default: {
      const _exhaustive: never = s;   // ❌ nếu thêm 'triangle' mà quên case
      return _exhaustive;
    }
  }
}

void — không return gì

void
// Function không cần trả về
function log(msg: string): void {
  console.log(msg);
  // không return, hoặc return; (không có giá trị)
}

const result = log('hi');   // result: void (gán được vào biến void hoặc any)
🧠 Mental model — 4 type này
  • any = "đừng nhìn vào đây" — TS bịt mắt.
  • unknown = "có gì đó, chưa biết là gì" — phải kiểm tra trước khi dùng.
  • never = "không bao giờ có giá trị" — code chết, dead branch.
  • void = "function chỉ làm side effect".

6. Literal type và Union type

TS không chỉ có type string, nó còn có type 'idle' — đúng là chuỗi 'idle' cụ thể. Đây là literal type.

Literal type
const mode: 'dev' = 'dev';     // type là literal 'dev', không phải string

// const tự infer literal, let infer rộng
const a = 'hello';             // type: 'hello'
let b = 'hello';               // type: string

Sức mạnh thật sự là khi kết hợp với union type (toán tử |) — "hoặc cái này hoặc cái kia".

Union type — pattern thay enum
type Status = 'idle' | 'loading' | 'success' | 'error';

let s: Status = 'idle';    // ✅
s = 'loading';             // ✅
s = 'done';                // ❌ TS2322

// Hỗ trợ IDE: gõ s = '|' tự suggest 4 giá trị
// Refactor: đổi tên 'idle' → 'pending' chỉ 1 click

// Union nhiều type khác nhau
type Id = string | number;
function format(id: Id): string {
  return `ID-${id}`;
}
💡 Literal union > enum

TS có enum (giống Java/C#) nhưng cộng đồng đã khuyến cáo tránh:

  • Enum không tan biến — sinh code JS runtime (function, object), khác hẳn type khác.
  • Bug nhỏ: const enum vs enum hành xử khác nhau, không tương thích isolatedModules.
  • Literal union đơn giản hơn: type Status = 'idle' | … chỉ là type, biến mất khi build.

Khi cần "object key string mapping": dùng as const object:

const Status = {
  Idle: 'idle',
  Loading: 'loading',
  Success: 'success',
  Error: 'error',
} as const;

type Status = typeof Status[keyof typeof Status];
// → 'idle' | 'loading' | 'success' | 'error'

7. type alias vs interface

2 cách đặt tên cho một shape (cấu trúc) object. Trông giống nhau:

Cả 2 đều mô tả object
// type alias
type User = {
  id: string;
  name: string;
  age: number;
};

// interface
interface User {
  id: string;
  name: string;
  age: number;
}

// Cả 2 dùng giống hệt:
const u: User = { id: '1', name: 'An', age: 22 };

Khác biệt thật sự:

Khả năngtypeinterface
Mô tả object shape
Mô tả primitive (type ID = string)
Union (A | B)
Intersection (A & B)chỉ qua extends
Tuple, mapped, conditional, utility
Declaration merging✅ (cùng tên → gộp)
extends kế thừaqua intersectioncú pháp đẹp extends
implements ở class

Declaration merging là khả năng độc đáo của interface:

Interface merging
interface User { id: string; }
interface User { name: string; }      // gộp lại
// Bây giờ User có { id, name }

const u: User = { id: '1', name: 'An' };  // ✅

// Với type — báo lỗi
type Cat = { name: string; };
type Cat = { age: number; };           // ❌ Duplicate identifier 'Cat'

Tính năng này hữu ích khi mở rộng type của thư viện bên thứ 3 (vd: thêm property cho Window, Express.Request) — gọi là module augmentation.

💡 Quy tắc thực dụng
  • interface cho shape của object/class — đặc biệt public API, library.
  • type cho union, intersection, primitive alias, tuple, utility.
  • Nếu phân vân: cứ type trước, gặp lúc cần merging mới đổi sang interface.
  • Đừng tranh cãi quá nhiều — team chọn một quy ước, hai cái đều ổn.

8. Intersection type &

Toán tử & ghép nhiều type thành một. Khác union (hoặc), intersection là "và" — phải thoả tất cả.

Intersection
type HasId = { id: string; };
type HasName = { name: string; };

type User = HasId & HasName;       // { id; name; }

const u: User = { id: '1', name: 'An' };    // ✅
const u2: User = { id: '1' };              // ❌ thiếu name

// Mix nhiều shape — pattern phổ biến (Mixin)
type Timestamped = { createdAt: Date; updatedAt: Date; };
type Soft = { deletedAt: Date | null; };

type AuditedUser = User & Timestamped & Soft;

So sánh nhanh extends (interface) vs & (type):

// Tương đương:
interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }

type Animal2 = { name: string; };
type Dog2 = Animal2 & { bark(): void; };
🔥 Gotcha — conflict trong intersection

Khi 2 type intersect mà có cùng property nhưng type khác nhau:

type A = { x: number; };
type B = { x: string; };
type C = A & B;       // C.x: never (giao giữa number và string = ∅)

const c: C = { x: 1 };    // ❌ không có giá trị nào vừa là number vừa là string

Property thành never — type C tồn tại nhưng không thể có instance.

9. ? Optional và readonly

Hai modifier hay dùng khi mô tả object: optional (có thể có hoặc không) và readonly (chỉ đọc).

Optional & readonly
interface User {
  readonly id: string;     // gán 1 lần, không sửa
  name: string;
  age?: number;             // optional — có thể vắng mặt
}

const u: User = { id: '1', name: 'An' };  // ✅ age vắng được

u.name = 'Bình';       // ✅
u.id = '2';            // ❌ Cannot assign to 'id' because it is a read-only property

// Optional → type thực = T | undefined
u.age;                // number | undefined

// Readonly array
const nums: readonly number[] = [1, 2, 3];
nums.push(4);          // ❌ Property 'push' does not exist on type 'readonly number[]'
nums[0] = 9;           // ❌
🔥 readonly chỉ là compile-time

Như mọi thứ trong TS, readonly không tồn tại lúc runtime. Compiler chặn code TS sửa, nhưng nếu một module JS bên ngoài (vd: thư viện) sửa thì hoàn toàn được. Đây khác hẳn Object.freeze (runtime).

Tương tự, const của JS (chương 2) chặn reassign biến runtime; readonly của TS chặn assign property compile-time. 2 thứ khác nhau.

10. Function types

Annotate parameter và return type — phần "lợi nhất" của TS ở mức cơ bản.

Function basic
// Function declaration
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function
const mul = (a: number, b: number): number => a * b;

// Return type thường có thể bỏ — inference biết
const sub = (a: number, b: number) => a - b;  // inferred: number

// Optional parameter (?)
function greet(name: string, title?: string) {
  return title ? `${title} ${name}` : name;
}

// Default parameter (đồng thời optional)
function power(x: number, n: number = 2) {
  return x ** n;
}

// Rest parameter
function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

Đặt tên cho kiểu của hàm — dùng type alias:

Function type alias
// Type alias cho signature
type BinaryOp = (a: number, b: number) => number;

const add: BinaryOp = (a, b) => a + b;     // a, b auto suy number
const mul: BinaryOp = (a, b) => a * b;

// Interface cho function — ít dùng nhưng có
interface Compare<T> {
  (a: T, b: T): number;
}

11. Type narrowing — thu hẹp union

Khi biến có union type, TS đủ thông minh để narrow (thu hẹp) sau mỗi điều kiện kiểm tra. Đây là cơ chế quan trọng nhất khi viết code TS.

typeof narrowing

typeof guard
function format(x: string | number): string {
  if (typeof x === 'string') {
    // trong nhánh này, x được narrow xuống string
    return x.toUpperCase();
  }
  // ở đây x chỉ còn là number
  return x.toFixed(2);
}

instanceof narrowing

instanceof guard
function describe(x: Date | string): string {
  if (x instanceof Date) {
    return x.toISOString();      // x: Date
  }
  return x;                       // x: string
}

in operator narrowing

"prop" in obj
type Fish = { swim(): void; };
type Bird = { fly(): void; };

function move(a: Fish | Bird) {
  if ('swim' in a) {
    a.swim();     // a: Fish
  } else {
    a.fly();      // a: Bird
  }
}

Type predicate (custom guard)

Khi check phức tạp hơn, tự viết function trả về x is T. Đây là type predicate — "nếu function trả true thì x là T".

Custom type guard
interface User { id: string; name: string; }

// Hàm kiểm tra runtime shape
function isUser(x: unknown): x is User {
  return (
    typeof x === 'object' &&
    x !== null &&
    'id' in x && typeof (x as any).id === 'string' &&
    'name' in x && typeof (x as any).name === 'string'
  );
}

function use(data: unknown) {
  if (isUser(data)) {
    console.log(data.name);    // ✅ data: User
  }
}
💡 Khi dùng predicate

Validation runtime cho dữ liệu từ ngoài (API response, JSON.parse, form input). Trong production, hay kết hợp với thư viện schema validation (Zod, Valibot, io-ts) — chúng tự sinh type guard chuẩn từ schema.

Discriminated union

Pattern siêu hữu ích: dùng một property literal làm discriminator để narrow.

Discriminated union
type Result<T> =
  | { ok: true;  value: T }
  | { ok: false; error: string };

function handle(r: Result<number>) {
  if (r.ok) {
    console.log(r.value);    // ✅ r: { ok: true; value: number }
  } else {
    console.log(r.error);    // ✅ r: { ok: false; error: string }
  }
}

12. Type assertion (as)

Khi bạn biết rõ type của value hơn TS (ví dụ DOM API), dùng as để "khẳng định". Nhưng phải hiểu: không có runtime check, chỉ là "trust me bro".

as assertion
// document.getElementById trả HTMLElement | null
// Ta biết đó là input → assert
const input = document.getElementById('email') as HTMLInputElement;
input.value = 'a@b.com';      // ✅ TS không phàn nàn

// Cú pháp cũ <Type> — không dùng trong .tsx vì xung đột JSX
const el = <HTMLInputElement>document.getElementById('email');

// Const assertion
const colors = ['red', 'green', 'blue'] as const;
// type: readonly ['red', 'green', 'blue']
// (không as const → string[])
🔥 as là chuyển sang chế độ "tin tôi"

as chỉ là compile-time. Runtime không có một byte code nào. Nếu sai → runtime error mơ hồ. Coi as như "tôi đã xác minh, đừng phàn nàn". Khi không chắc chắn → không dùng as, dùng type guard / narrowing.

const x = 'hello' as unknown as number;   // "double assertion" — ép bằng mọi giá
x.toFixed(2);   // 💥 runtime: x.toFixed is not a function
🧠 So sánh — Dart sound null safety

Cả TS (strict: true) và Dart 3 đều ép developer xử lý nullable rõ ràng:

// TypeScript
let x: string | null = null;
x.length;          // ❌
x!.length;         // non-null assertion (chính xác như `as`)
if (x) x.length;  // ✅ narrowing

// Dart (sound null safety)
String? x = null;
x.length;          // ❌
x!.length;         // non-null assertion (runtime check, throw nếu null!)
if (x != null) x.length;  // ✅ narrowing

Khác biệt mấu chốt: Dart sound — runtime kiểm tra thật, ! throw nếu null. TS compile-time only! không có check runtime, có thể fail âm thầm. Đây là giá phải trả khi TS phải tương thích với JS có sẵn.

Bài tập

Bài 1 — Setup project TS từ con số 0

Tạo project mới, viết file hello.ts, chạy được bằng tsx.

  1. Tạo folder mới: mkdir my-ts && cd my-ts
  2. npm init -y
  3. npm install -D typescript tsx @types/node
  4. npx tsc --init → mở tsconfig.json, đảm bảo strict: true
  5. Tạo hello.ts: const name: string = 'TS'; console.log(`Hello ${name}`);
  6. Chạy: npx tsx hello.ts
  7. Kiểm tra type: npx tsc --noEmit (không có lỗi)
  8. Thử cố tình gây lỗi: gán name = 42 → chạy lại tsc --noEmit → thấy error TS2322
Đáp án — script package.json gợi ý
"scripts": {
  "dev": "tsx watch hello.ts",
  "check": "tsc --noEmit",
  "build": "tsc",
  "start": "node dist/hello.js"
}

Sau đó npm run dev để dev, npm run check để CI kiểm tra type.

Bài 2 — Convert JS → TS

Lấy hàm JS sau và convert sang TS với type đầy đủ. Bước 1 dùng any, bước 2 thay bằng type cụ thể.

// Original JS
function processOrder(order, discount) {
  const total = order.items.reduce((sum, it) => sum + it.price * it.qty, 0);
  const final = total * (1 - (discount ?? 0));
  return { id: order.id, total: final, items: order.items.length };
}
Đáp án — bước 1 (any), bước 2 (typed)

Bước 1 — quick & dirty với any (đáng tránh nhưng đôi khi cần):

function processOrder(order: any, discount: any): any { /* ... */ }

Bước 2 — typed thật sự:

interface Item {
  price: number;
  qty: number;
}
interface Order {
  id: string;
  items: Item[];
}
interface OrderSummary {
  id: string;
  total: number;
  items: number;
}

function processOrder(order: Order, discount?: number): OrderSummary {
  const total = order.items.reduce((sum, it) => sum + it.price * it.qty, 0);
  const final = total * (1 - (discount ?? 0));
  return { id: order.id, total: final, items: order.items.length };
}

Bài 3 — Result<T, E> discriminated union

Định nghĩa generic Result<T, E> như discriminated union, rồi viết safeParse(json: string): Result<unknown, Error>.

  • Nếu parse thành công → { ok: true, value: ... }
  • Nếu fail → { ok: false, error: Error }
Đáp án
type Result<T, E = Error> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

function safeParse(json: string): Result<unknown, Error> {
  try {
    return { ok: true, value: JSON.parse(json) };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
  }
}

const r = safeParse('{"a":1}');
if (r.ok) {
  console.log(r.value);    // narrowed: unknown — tiếp tục validate ở bước sau
} else {
  console.error(r.error.message);
}

Pattern này thay thế try/catch rải rác — caller phải xử lý cả 2 nhánh, không bao giờ quên error.

Bài 4 — Custom type guard

Viết isApiError(e: unknown): e is { status: number; message: string } kiểm tra shape runtime. Sau đó dùng nó:

try {
  await fetch('...');
} catch (e) {
  if (isApiError(e)) {
    console.log(e.status, e.message);  // type-safe
  } else {
    throw e;
  }
}
Đáp án
function isApiError(e: unknown): e is { status: number; message: string } {
  return (
    typeof e === 'object' &&
    e !== null &&
    'status' in e &&
    typeof (e as any).status === 'number' &&
    'message' in e &&
    typeof (e as any).message === 'string'
  );
}

Một bộ check 5 điều kiện: object, không null, có key đúng, type giá trị đúng. Lib Zod thay cho việc tự viết — nhưng bài này cốt để hiểu cơ chế.

Bài 5 — Refactor file JS → TS, xoá hết any

Lấy một file .js nhỏ bạn đã viết ở chương 6-8 (vd: module fetch user, util array). Đổi đuôi sang .ts, chạy tsc --noEmit:

  1. TS sẽ phàn nàn về implicit any. Tạm thêm : any để chạy được.
  2. Sau đó duyệt từng any và thay bằng type cụ thể (define interface/type nếu cần).
  3. Mục tiêu: tsc --noEmit không lỗi không còn từ any nào trong file.
Gợi ý chiến thuật
  • Bật rule ESLint @typescript-eslint/no-explicit-any để bắt any tự động.
  • API response không tin được → annotate là unknown, rồi viết type guard ở biên.
  • Callback của thư viện ngoài: tra node_modules/@types/... hoặc typescriptlang.org/dt/search.
  • Khi bí thật: tạm as unknown as TargetType, kèm comment // TODO: validate runtime.

Quiz

Q1

Khi build TS ra JS, type annotation có còn trong file output không? Type check có chạy ở runtime không?

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

Không và không. Compiler tsc xoá toàn bộ annotation — file .js xuất ra không chứa : string, : number… Toàn bộ kiểm tra type là compile-time. Runtime vẫn là JS thuần, vẫn có typeof, vẫn có coercion. Muốn check runtime: tự viết type guard, hoặc dùng Zod/Valibot.

Q2

Khác nhau giữa anyunknown? Khi nào dùng cái nào?

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

any tắt type check — bạn có thể làm bất cứ gì với nó (gọi method, truy cập property, gán đi đâu cũng được). Backdoor nguy hiểm.

unknown đối ngược — TS biết có gì đó nhưng không cho bạn làm gì cho đến khi narrow (typeof, instanceof, type guard). An toàn hơn nhiều.

Quy tắc: luôn chọn unknown. Nếu thật sự cần any (vd code cũ chưa convert), thêm comment // TODO: type later.

Q3

Một interface có thể extends một type alias không?

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

— miễn là type đó mô tả object shape. interface Dog extends AnimalType { … } hoàn toàn hợp lệ. Nhưng nếu AnimalType là union/primitive, không extends được.

Ngược lại cũng vậy: type Dog = Animal & { bark(): void } hoạt động với cả interface và type alias bên trái.

Q4

as const làm gì cho một array literal?

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

2 việc cùng lúc:

  1. Chuyển array thành readonly tuple (không push/pop được).
  2. Suy từng phần tử thành literal type chứ không phải type rộng.
const colors = ['red', 'green'] as const;
// type: readonly ['red', 'green']

// Không có as const:
const colors2 = ['red', 'green'];
// type: string[]

Dùng phổ biến để tạo "enum like" object hoặc fix giá trị literal cho discriminated union.

Q5

Khi nào chọn type, khi nào chọn interface?

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

interface: object shape, class implement, public API library cần declaration merging (vd: extend Window, Express.Request).

type: union, intersection, tuple, primitive alias, mapped/conditional type, utility type. Tất cả những gì không phải shape đơn thuần.

Nếu nhỏ và shape đơn giản — chọn cái nào cũng ok, miễn nhất quán trong team. Khi viết library: ưu tiên interface cho object public.

Q6

Với strictNullChecks bật, nullundefined có là subtype của mọi type không?

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

Không. Với strictNullChecks: true (mặc định trong strict), nullundefined là 2 type riêng. Phải khai báo rõ: string | null nếu muốn cho phép null.

Khi tắt strictNullChecks (legacy mode): null/undefined assign vào mọi type được — đây là lý do TS thời kỳ đầu vẫn để lọt bug "cannot read property of undefined". Bật strict là bắt buộc cho dự án mới.

Q7

x as HTMLInputElement có sinh runtime check không? Nếu sai thì sao?

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

Không có runtime check. as hoàn toàn compile-time — biên dịch ra JS xong, byte code của x as Foo giống hệt như x.

Nếu sai (vd x thật ra là null hoặc HTMLDivElement), TS không phát hiện. Runtime sẽ throw lỗi khi bạn gọi .value trên null, hoặc silently sai logic khác.

Đó là lý do nên ưu tiên type guard (có runtime check) hơn as (không có). Dùng as chỉ cho các case TS quá rộng nhưng bạn chắc chắn (vd DOM API).

Tổng kết

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

  • TS = JS + static type. Type biến mất khi build, runtime vẫn là JS thuần.
  • Setup: typescript + tsx, luôn strict: true.
  • Primitive types: string, number, boolean, … — và sức mạnh của inference.
  • 4 special types: any (tránh), unknown (ưa thích), never (dead branch), void (không return).
  • Literal + union thay enum: 'idle' | 'loading' | ….
  • type vs interface: type linh hoạt, interface có declaration merging.
  • Intersection &: mix shape, khác extends nhưng tương đương.
  • Optional ?, readonly: 2 modifier compile-time.
  • Function: annotate parameter, return type — phần lợi nhất.
  • Narrowing: typeof, instanceof, in, type predicate, discriminated union.
  • Assertion as: compile-time "trust me", không runtime check.

Kết nối

  • Chương 10 (TS Generics & Utility Types) — viết function/type tái sử dụng với type parameter.
  • Chương 11 (Advanced Types) — mapped, conditional, infer, template literal type.
  • Chương 12 (TS in Practice) — Zod validation, TS + React, TS + Node API.
  • Chương 2 (JS Types) — type runtime của JS là thứ TS phải đối phó.
  • Dart Chương 2 — sound null safety của Dart vs strict null check của TS. Đối chiếu để hiểu trade-off.