TypeScript Handbook thực chiến cho lập trình viên JavaScript
Đây là tài liệu Reference Guide / Cheat Sheet. Bài viết được cấu trúc để tra cứu nhanh: mỗi mục gồm Định nghĩa → Vấn đề giải quyết → Code thực chiến → Trade-offs. Mọi nội dung kỹ thuật đã được đối chiếu với tài liệu gốc tại typescriptlang.org.
Agenda
Thời gian đọc ước tính: ~40 phút
Learning Outcomes
Sau khi đọc xong bài này, bạn có thể:
- Giải thích được sự khác biệt cốt lõi giữa
any,unknown, vànever— và biết khi nào nên dùng loại nào - Áp dụng đúng các kỹ thuật Type Narrowing (
typeof,instanceof,in, type predicates) để viết code an toàn - Phân biệt được khi nào dùng
interfacevà khi nào dùngtype aliasdựa trên hành vi thực tế của compiler - Tự viết Generic Functions với Constraints để tái sử dụng logic mà không mất type safety
- Chọn đúng Utility Type (
Partial,Omit,Pick,Record,ReturnType...) cho từng bài toán API cụ thể
Prerequisites
- Biết JavaScript ES6+ (arrow functions, destructuring, async/await)
- Đã dùng TypeScript ít nhất một lần trong dự án
Glossary & Vocabulary
1. Technical Terms (Thuật ngữ kỹ thuật):
| Term | Vietnamese Meaning & Quick Explain |
|---|---|
| Static Type Checking | Kiểm tra kiểu dữ liệu lúc biên dịch (trước khi chạy). Đối lập với Dynamic Typing của JavaScript thuần. |
| Type Inference | Trình biên dịch tự suy luận ra kiểu dữ liệu dựa trên giá trị được gán, không cần khai báo tường minh. |
| Narrowing | Quá trình thu hẹp type từ loại rộng (union type) xuống loại cụ thể hơn trong một block code. |
| Type Predicate | Return type dạng param is Type trong user-defined type guards, báo cho compiler biết kết quả narrowing. |
| Declaration Merging | Khả năng của interface cho phép khai báo cùng tên nhiều lần, compiler tự động gộp lại. |
| Generic Constraint | Giới hạn tập hợp kiểu mà type parameter T có thể nhận, dùng cú pháp T extends SomeType. |
| Utility Type | Các type transformation built-in của TypeScript (Partial<T>, Pick<T, K>...) để biến đổi type có sẵn. |
| Discriminated Union | Union type có một property chung (discriminant) với literal type, giúp compiler narrow chính xác. |
| Exhaustive Check | Kỹ thuật dùng never để đảm bảo mọi nhánh của switch/if đều được xử lý. |
| Decorator | Syntax @expression để gắn thêm hành vi vào class, method, property, parameter — bản chất là Higher-Order Function. |
| Structural Typing | TypeScript kiểm tra type dựa trên hình dạng (shape/structure) của object, không phải tên type. |
2. Vocabulary Support (Từ vựng học thuật):
| Word | Meaning in Context |
|---|---|
| Assignable (adj) | Có thể được gán vào — ví dụ: string is assignable to string | number. |
| Coercion (n) | Ép kiểu tự động (implicit type conversion) như JavaScript ép 0 thành false. |
| Ambient (adj) | Môi trường khai báo type-only, không có implementation — thường dùng trong .d.ts. |
| Constraint (n) | Ràng buộc — điều kiện mà type parameter phải thỏa mãn. |
| Infer (v) | Suy luận — compiler tự động xác định type từ context mà không cần khai báo tường minh. |
1. Vấn đề kỹ thuật mà TypeScript giải quyết
Các dự án JavaScript ở quy mô lớn gặp phải các vấn đề có tính hệ thống:
- Lỗi kiểu dữ liệu xuất hiện ở runtime: Gọi
.toUpperCase()trên một giá trịnull→ crash ở môi trường production, không bị bắt lúc development. - Refactoring rủi ro cao: Đổi tên một property → toàn bộ nơi sử dụng có thể vỡ mà không có cách phát hiện tự động.
- IDE không đủ thông tin: Autocompletion không chính xác vì không biết shape của object nhận từ API.
- Onboarding khó: Code không self-documenting — người mới phải đọc implementation mới biết function nhận/trả về gì.
TypeScript giải quyết bằng Static Type Checking (kiểm tra kiểu tĩnh): compiler phân tích code trước khi chạy và báo lỗi ngay tại thời điểm viết code.
Quan trọng từ docs chính thống: TypeScript là một structural type system — compiler quan tâm đến shape (hình dạng) của type, không phải tên của nó. Nếu hai type có cùng structure, chúng tương thích nhau. (source)
2. TypeScript Types
2.1 Primitive Types (Kiểu nguyên thủy)
TypeScript map 1-1 với các primitive của JavaScript. Theo tài liệu chính thống, luôn dùng chữ thường (string, number, boolean) — không dùng String, Number, Boolean (chữ hoa) vì đây là built-in types đặc biệt ít khi cần dùng.
// filename: types/primitives.ts
const productName: string = "iPhone 16 Pro";
const price: number = 29_990_000; // Dấu _ làm separator cho dễ đọc — valid JavaScript/TypeScript
const inStock: boolean = true;
// null và undefined: hành vi phụ thuộc vào strictNullChecks trong tsconfig
let discountCode: string | null = null;
let expiryDate: Date | undefined;
Trade-off khi bật strictNullChecks:
- Bật: Compiler bắt mọi trường hợp có thể null/undefined → code an toàn hơn nhưng phải xử lý thêm
- Tắt:
nullvàundefinedcó thể gán vào bất kỳ type nào → dễ bị runtime error
Từ docs: "We always recommend people turn
strictNullCheckson if it's practical to do so." — TypeScript Handbook, Everyday Types
2.2 Object Types (Kiểu đối tượng)
// filename: types/product.ts
// Cách 1: Inline object type — dùng cho parameter nhỏ, không tái sử dụng
function displayProduct(product: { id: string; name: string; price: number }) {
console.log(`${product.name}: ${product.price.toLocaleString("vi-VN")} VND`);
}
// Cách 2: Type alias — RECOMMENDED cho production code vì tái sử dụng được
type Product = {
id: string;
name: string;
price: number;
category: string;
imageUrl?: string; // ? = optional property — không bắt buộc phải có
readonly sku: string; // readonly = không được gán lại sau khi khởi tạo
};
const laptop: Product = {
id: "p-001",
name: "MacBook Pro M4",
price: 49_990_000,
category: "Laptop",
sku: "MBP-M4-512"
};
// Compiler bắt lỗi: Cannot assign to 'sku' because it is a read-only property.
// laptop.sku = "MBP-M4-1TB";
Lưu ý về Type Inference: Trong hầu hết trường hợp, TypeScript tự suy luận type từ giá trị được gán. Bạn không cần khai báo tường minh nếu giá trị đủ rõ ràng:
// TypeScript tự infer: myName: string
let myName = "Alice";
// Tương đương với
let myName: string = "Alice"; // Thừa — không cần thiết
2.3 Top Types: any vs unknown
Đây là phân biệt quan trọng nhất mà phần lớn lập trình viên mới dùng TypeScript hiểu sai.
Definition Anatomy — unknown type:
Định nghĩa từ docs: "The unknown type represents any value. This is similar to the any type, but is safer because it's not legal to do anything with an unknown value."
Giải phẫu:
- any value (bất kỳ giá trị nào):
unknownnhận tất cả — giốngany - safer (an toàn hơn): không thể dùng trực tiếp — phải kiểm tra type trước
- not legal to do anything (không được phép làm gì): compiler từ chối mọi thao tác trừ khi đã narrow down
any | unknown | |
|---|---|---|
| Nhận giá trị gì? | Bất kỳ | Bất kỳ |
| Dùng trực tiếp không cần check? | Được | KHÔNG — phải narrow trước |
| Type safety | Tắt hoàn toàn | Được bảo vệ |
| Khi nào dùng? | Cực kỳ hiếm (legacy migration) | Data từ bên ngoài (API, user input) |
// filename: services/api.service.ts
// Antipattern: any — compiler "mù" hoàn toàn
async function fetchUserDataDangerous(userId: string): Promise<any> {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// data.fullname.toUpperCase() → không có lỗi compile nhưng crash lúc runtime
// Đúng: unknown — buộc phải kiểm tra trước khi dùng
async function fetchUserData(userId: string): Promise<unknown> {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
// Muốn dùng phải narrow down (xem phần 4 — Type Narrowing)
const rawData = await fetchUserData("usr-123");
if (typeof rawData === "object" && rawData !== null && "name" in rawData) {
// Bây giờ TypeScript mới cho phép dùng
console.log((rawData as { name: string }).name);
}
Từ docs: Compiler flag noImplicitAny sẽ báo lỗi khi TypeScript không thể infer type và phải fallback về any. Nên bật flag này trong mọi dự án production.
2.4 Bottom Type: never
Definition Anatomy:
Định nghĩa từ docs: "When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn't exist."
- removed all possibilities (đã loại bỏ tất cả khả năng): không còn nhánh nào có thể xảy ra
- state which shouldn't exist (trạng thái không nên tồn tại): code đến đây là bất khả thi về mặt logic
Tính chất quan trọng từ docs:
neverlà assignable to (có thể gán vào) mọi type- Nhưng không có type nào assignable to
never(ngoại trừneverchính nó)
Ứng dụng thực chiến: Exhaustive Check trong switch-case
// filename: services/order.service.ts
type OrderStatus = "pending" | "processing" | "shipped" | "delivered";
function getStatusMessage(status: OrderStatus): string {
switch (status) {
case "pending":
return "Đơn hàng đang chờ xác nhận";
case "processing":
return "Đang đóng gói và chuẩn bị giao";
case "shipped":
return "Đơn hàng đang trên đường vận chuyển";
case "delivered":
return "Đã giao thành công";
default:
// Gán vào never: nếu TypeScript chưa xử lý hết case, dòng này báo lỗi compile
// Khi team thêm "cancelled" vào OrderStatus mà quên xử lý switch, compiler chỉ ra ngay
const _exhaustiveCheck: never = status;
throw new Error(`Unhandled order status: ${_exhaustiveCheck}`);
}
}
Sơ đồ minh họa cơ chế Exhaustive Check:
2.5 Type Assertion (Khẳng định kiểu)
Khi nào dùng: Khi bạn có thông tin về type mà compiler không có — ví dụ khi làm việc với DOM API trả về generic HTMLElement.
// filename: utils/dom.utils.ts
// Cú pháp as Type — RECOMMENDED, hoạt động trong cả file .tsx
const emailInput = document.getElementById("email-input") as HTMLInputElement;
emailInput.value = "user@example.com"; // Compiler biết đây là input, có property .value
// Cú pháp <Type> — KHÔNG dùng trong .tsx vì xung đột với JSX syntax
const usernameInput = <HTMLInputElement>document.getElementById("username-input");
Lưu ý quan trọng từ docs: Type assertions bị xóa khi biên dịch — không có runtime checking. Nếu assertion sai, KHÔNG có exception hay null được throw. TypeScript chỉ cho phép assertion khi type "chồng lấn" nhau (ví dụ string không thể assert sang number).
// Compiler từ chối: string và number không overlap đủ
// const x = "hello" as number; // Error
// Khi cần double assertion — dùng unknown làm trung gian
const weirdCase = someValue as unknown as SpecificType;
as const — Kỹ thuật tạo Literal Types từ Array/Object:
// filename: config/payment.ts
// KHÔNG có as const: TypeScript infer type là string[] — mất literal type
const METHODS_MUTABLE = ["credit_card", "bank_transfer", "momo"];
// type: string[]
// Với as const: freeze thành readonly tuple với literal types
const PAYMENT_METHODS = ["credit_card", "bank_transfer", "momo"] as const;
// type: readonly ["credit_card", "bank_transfer", "momo"]
// Tạo union type từ array tự động — single source of truth
type PaymentMethod = typeof PAYMENT_METHODS[number];
// type: "credit_card" | "bank_transfer" | "momo"
// Với object — giữ nguyên literal value thay vì widen thành number
const HTTP_STATUS = {
OK: 200,
NOT_FOUND: 404,
INTERNAL_ERROR: 500,
} as const;
type HttpStatusCode = typeof HTTP_STATUS[keyof typeof HTTP_STATUS];
// type: 200 | 404 | 500
Từ docs: "The
as constsuffix acts likeconstbut for the type system, ensuring that all properties are assigned the literal type instead of a more general version likestringornumber." — Everyday Types
3. Combining Types (Kết hợp kiểu)
3.1 Union Types (|)
Definition: "A union type is a type formed from two or more other types, representing values that may be any one of those types." — TypeScript Handbook
// filename: types/notification.ts
type NotificationChannel = "email" | "sms" | "push";
// Union types thực chiến: Discriminated Union pattern
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string; errorCode: number };
// TypeScript narrow tự động dựa vào property "success"
function handleUserResponse(response: ApiResponse<User>) {
if (response.success) {
// Compiler biết chắc response.data tồn tại ở đây (Discriminated Union)
displayUserProfile(response.data);
} else {
// Compiler biết chắc response.error và response.errorCode tồn tại
showErrorToast(`${response.error} (Code: ${response.errorCode})`);
}
}
Lưu ý từ docs: Khi làm việc với union type, TypeScript chỉ cho phép thao tác nếu thao tác đó hợp lệ với mọi member của union. Ví dụ string | number không cho phép gọi .toUpperCase() trực tiếp vì number không có method này.
3.2 Intersection Types (&)
Definition: Gộp nhiều type lại thành một type phức tạp hơn. Giá trị phải thỏa mãn đồng thời tất cả các type được gộp.
// filename: types/user.ts
type BaseEntity = {
id: string;
createdAt: Date;
updatedAt: Date;
};
type UserProfile = {
fullName: string;
email: string;
avatarUrl?: string;
};
type AdminCapabilities = {
permissions: string[];
canDeleteContent: boolean;
};
// Intersection: User phải có đầy đủ properties từ cả 2 type
type User = UserProfile & BaseEntity;
// Thêm tầng nữa
type AdminUser = User & AdminCapabilities;
// adminUser PHẢI có tất cả fields từ cả 3 type
const adminUser: AdminUser = {
id: "usr-admin-001",
createdAt: new Date(),
updatedAt: new Date(),
fullName: "Nguyễn Quản Trị",
email: "admin@company.com",
permissions: ["user:read", "user:write"],
canDeleteContent: true,
};
4. Type Guards & Narrowing (Thu hẹp kiểu)
Definition Anatomy:
Định nghĩa từ docs: "TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position."
- paths of execution (đường thực thi): các nhánh if/else, switch, loop mà code có thể đi qua
- most specific possible type (kiểu cụ thể nhất có thể): sau khi đã loại bỏ các type không phù hợp
- at a given position (tại một vị trí cụ thể): trong một block code cụ thể
4.1 typeof Guards
Theo docs, typeof trả về một trong các chuỗi: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", "function".
Gotcha quan trọng từ docs: typeof null === "object" — đây là một quirk lịch sử của JavaScript. Khi check object, phải xử lý thêm null case:
// filename: utils/formatter.ts
type RawValue = string | number | Date;
function formatDisplayValue(value: RawValue): string {
if (typeof value === "string") {
return value.trim().toUpperCase(); // Compiler biết value là string
}
if (typeof value === "number") {
return value.toLocaleString("vi-VN"); // Compiler biết value là number
}
// Sau 2 check trên, TypeScript dùng Control Flow Analysis biết đây chỉ có thể là Date
return value.toLocaleDateString("vi-VN");
}
4.2 instanceof Guards
instanceof hoạt động dựa trên prototype chain — phù hợp với class instances.
function logValue(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString()); // x: Date
} else {
console.log(x.toUpperCase()); // x: string
}
}
4.3 in Operator Narrowing
Theo docs: "JavaScript has an operator for determining if an object or its prototype chain has a property with a name." TypeScript dùng in để narrow union types.
// filename: types/payment.ts
type CreditCardPayment = {
method: "credit_card";
cardNumber: string;
cvv: string;
};
type BankTransferPayment = {
method: "bank_transfer";
bankAccount: string;
bankCode: string;
};
type Payment = CreditCardPayment | BankTransferPayment;
function processPaymentDetails(payment: Payment) {
if ("cardNumber" in payment) {
// TypeScript narrow xuống CreditCardPayment
console.log(`Charging card ending in ${payment.cardNumber.slice(-4)}`);
} else {
// TypeScript narrow xuống BankTransferPayment
console.log(`Transferring to account ${payment.bankAccount}`);
}
}
Lưu ý từ docs: Optional properties sẽ xuất hiện ở cả hai phía của in check. Ví dụ: nếu Human có swim?: () => void, thì "swim" in human vẫn có thể true hoặc false.
4.4 Discriminated Unions (Union có discriminant)
Đây là pattern mạnh mẽ nhất được docs đề xuất cho union types phức tạp. Mỗi type trong union có một property chung với literal type riêng biệt.
// filename: types/shape.ts
// Thiết kế ĐÚNG: mỗi type có discriminant property riêng
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
// Khi check kind, TypeScript narrow chính xác
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // shape: Circle
case "square":
return shape.sideLength ** 2; // shape: Square
default:
// Exhaustive check với never
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
4.5 User-defined Type Guards (Type Predicate is)
Dùng khi typeof và instanceof không đủ — cần kiểm tra structure của object.
// filename: types/api-response.ts
type SuccessResponse = {
status: "success";
data: { userId: string; accessToken: string };
};
type ErrorResponse = {
status: "error";
message: string;
code: number;
};
type AuthResponse = SuccessResponse | ErrorResponse;
// Return type "response is SuccessResponse" là type predicate
// Khi hàm này return true → bên trong if block, compiler hiểu response là SuccessResponse
function isSuccessResponse(response: AuthResponse): response is SuccessResponse {
return response.status === "success";
}
async function handleLogin(credentials: { email: string; password: string }) {
const response: AuthResponse = await loginApi(credentials);
if (isSuccessResponse(response)) {
// TypeScript biết chắc đây là SuccessResponse
localStorage.setItem("token", response.data.accessToken);
} else {
// TypeScript biết chắc đây là ErrorResponse
showAlert(`Login failed: ${response.message} (${response.code})`);
}
}
Bonus từ docs: Type guards cũng có thể dùng để filter array:
const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet()];
const underwater: Fish[] = zoo.filter(isFish); // Compiler biết kết quả là Fish[]
5. Interface
5.1 Khai báo và extends
// filename: types/catalog.ts
interface BaseProduct {
id: string;
name: string;
price: number;
}
// extends: kế thừa và mở rộng — không làm mất type gốc
interface PhysicalProduct extends BaseProduct {
weight: number;
dimensions: { width: number; height: number; depth: number };
shippingClass: "standard" | "express" | "bulky";
}
// Extends nhiều interface cùng lúc (không thể làm với type alias)
interface BundleProduct extends PhysicalProduct, DigitalProduct {
bundledItems: string[];
}
5.2 implements với Class
// filename: services/payment.service.ts
interface PaymentGateway {
charge(amount: number, currency: string): Promise<{ transactionId: string }>;
refund(transactionId: string, amount: number): Promise<boolean>;
}
// Class PHẢI implement đầy đủ tất cả methods — compiler báo lỗi nếu thiếu
class StripeGateway implements PaymentGateway {
async charge(amount: number, currency: string) {
const result = await stripe.charges.create({ amount, currency });
return { transactionId: result.id };
}
async refund(transactionId: string, amount: number) {
await stripe.refunds.create({ charge: transactionId, amount });
return true;
}
}
// Cả hai gateway đáp ứng cùng contract → dùng hoán đổi nhau được
function processOrder(gateway: PaymentGateway, amount: number) {
return gateway.charge(amount, "VND");
}