- 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
typealias 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:
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:
- Type chỉ tồn tại lúc compile. Khi code đã build ra
.js, không còn dấu vết: string,: numbernào. Runtime vẫn là JS thuần — vẫn cótypeof, vẫn có coercion, vẫn cóNaN(chương 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.3vẫn đúng trong TS. - 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:
// 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
// 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
"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.
# 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 |
|---|---|---|
tsc | Compiler chính thức. Đọc .ts → kiểm type → xuất .js. | Build production, --noEmit để CI check type. |
tsx | Wrapper 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-node | Wrapper trên tsc — chạy .ts + type check (chậm hơn). | Cũ, hiện ít dùng. tsx thay thế. |
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.
{
"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ĩa | Giá trị nên dùng |
|---|---|---|
target | JS phiên bản output (ES5, ES2015, …, ESNext). | ES2022 cho Node hiện đại / browser ≥2023. |
module | Module system output: CommonJS, ESNext, NodeNext. | NodeNext cho server, ESNext cho bundler. |
strict | Bật cùng lúc 8 flags strict (noImplicitAny, strictNullChecks, …). | Luôn bật true. |
esModuleInterop | Hỗ trợ import x from 'mod' với module CommonJS. | true. |
skipLibCheck | Bỏ qua kiểm tra type trong node_modules. | true (tăng tốc 5-10×). |
outDir | Thư mục xuất .js. | "./dist". |
noUncheckedIndexedAccess | Truy cập arr[i] trả về T | undefined. | true (cứng hơn, ít bug hơn). |
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.
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.
// 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'
- 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ĩa | Có 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ỉ undefined | Không gọi được |
any — backdoor nguy hiểm
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
any là backdoor 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
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 là "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
// 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ì
// 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)
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.
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".
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}`;
}
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 enumvsenumhà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:
// 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ăng | type | interface |
|---|---|---|
| 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ừa | qua intersection | cú pháp đẹp extends |
| implements ở class | ✅ | ✅ |
Declaration merging là khả năng độc đáo của interface:
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.
interfacecho shape của object/class — đặc biệt public API, library.typecho union, intersection, primitive alias, tuple, utility.- Nếu phân vân: cứ
typetrước, gặp lúc cần merging mới đổi sanginterface. - Đừ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ả.
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; };
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).
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 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:
// 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
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
function describe(x: Date | string): string {
if (x instanceof Date) {
return x.toISOString(); // x: Date
}
return x; // x: string
}
in operator narrowing
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".
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
}
}
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.
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".
// 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
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.
- Tạo folder mới:
mkdir my-ts && cd my-ts npm init -ynpm install -D typescript tsx @types/nodenpx tsc --init→ mởtsconfig.json, đảm bảostrict: true- Tạo
hello.ts:const name: string = 'TS'; console.log(`Hello ${name}`); - Chạy:
npx tsx hello.ts - Kiểm tra type:
npx tsc --noEmit(không có lỗi) - Thử cố tình gây lỗi: gán
name = 42→ chạy lạitsc --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:
- TS sẽ phàn nàn về implicit
any. Tạm thêm: anyđể chạy được. - Sau đó duyệt từng
anyvà thay bằng type cụ thể (defineinterface/typenếu cần). - Mục tiêu:
tsc --noEmitkhông lỗi và không còn từanynào trong file.
Gợi ý chiến thuật
- Bật rule ESLint
@typescript-eslint/no-explicit-anyđể bắtanytự độ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
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
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.
Khác nhau giữa any và unknown? Khi nào dùng cái nào?
Xem đá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.
Một interface có thể extends một type alias không?
Xem đáp án
Có — 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.
as const làm gì cho một array literal?
Xem đáp án
2 việc cùng lúc:
- Chuyển array thành readonly tuple (không push/pop được).
- 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.
Khi nào chọn type, khi nào chọn interface?
Xem đá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.
Với strictNullChecks bật, null và undefined có là subtype của mọi type không?
Xem đáp án
Không. Với strictNullChecks: true (mặc định trong strict),
null và undefined 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.
x as HTMLInputElement có sinh runtime check không? Nếu sai thì sao?
Xem đá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ônstrict: 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' | …. typevsinterface:typelinh hoạt,interfacecó 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.