Mục lục
Giới Thiệu
TypeScript đã trở thành một công cụ không thể thiếu trong thế giới phát triển web hiện đại, mang lại khả năng kiểm tra kiểu mạnh mẽ và giúp phát hiện lỗi sớm. Tuy nhiên, nhiều lập trình viên chỉ dừng lại ở các kiểu dữ liệu cơ bản. Để thực sự khai thác toàn bộ tiềm năng của TypeScript và viết code chất lượng cao, dễ bảo trì hơn, bạn cần tìm hiểu sâu hơn về các tính năng nâng cao mà ngôn ngữ này cung cấp.
Bài viết này sẽ đưa bạn “vượt xa kiến thức cơ bản”, khám phá 21 tính năng TypeScript độc đáo mà bạn có thể chưa biết. Từ việc tăng cường an toàn dữ liệu đến việc tạo ra các kiểu phức tạp một cách linh hoạt, những kiến thức này sẽ giúp bạn nâng cao kỹ năng lập trình và tối ưu hóa quy trình phát triển của mình.
1. Mảng, Tuple `readonly` và Khẳng Định `as const`
Mặc định, mảng và đối tượng trong JavaScript là mutable (có thể thay đổi). TypeScript cố gắng suy luận kiểu rộng hơn cho các giá trị literal, điều này có thể dẫn đến việc mất thông tin kiểu cụ thể và khó khăn trong việc bắt lỗi hoặc cung cấp gợi ý tự động chính xác.
const colors = ["red", "green", "blue"];
// Kiểu: string[] - có thể chứa bất kỳ chuỗi nào
colors.push("yellow"); // Được phép, nhưng có thể không phải điều bạn muốn
type Color = (typeof colors)[number]; // string (quá chung chung!)
Giải Pháp
Sử dụng `as const` để biến mọi thứ thành `readonly` và bảo toàn kiểu literal, hoặc dùng từ khóa `readonly` cho các mảng và tuple cụ thể.
const colors = ["red", "green", "blue"] as const;
// Kiểu: readonly ["red", "green", "blue"]
colors.push("yellow"); // ✗ Lỗi: không thể sửa đổi mảng readonly
type Color = (typeof colors)[number]; // "red" | "green" | "blue" ✓
// Hoặc cho tham số hàm:
function display(items: readonly string[]) {
items.push("x"); // ✗ Lỗi: không thể sửa đổi
items.forEach(console.log); // ✓ OK: đọc thì được
}
Khi Nào Nên Sử Dụng
- Dữ liệu cấu hình hoặc hằng số không nên thay đổi.
- Ngăn chặn các sửa đổi không mong muốn.
- Bảo toàn kiểu literal để suy luận kiểu chính xác hơn.
- Tham số hàm mà bạn không muốn chúng bị thay đổi bên trong hàm.
Tìm hiểu thêm: TypeScript Docs: ReadonlyArray
2. `keyof typeof` cho Enum dạng Object-as-Const
Các `enum` truyền thống của TypeScript đôi khi có những hành vi riêng và tạo ra mã JavaScript bổ sung. Trong một số trường hợp, bạn chỉ muốn định nghĩa các hằng số trong một đối tượng thuần túy và từ đó suy ra các kiểu.
Giải Pháp
Kết hợp `as const` (để khóa các giá trị literal), `typeof` (để lấy kiểu của đối tượng), và `keyof` (để lấy union của các khóa hoặc giá trị).
// Định nghĩa các hằng số của bạn dưới dạng một đối tượng thuần
const STATUS = {
PENDING: "pending",
APPROVED: "approved",
REJECTED: "rejected",
} as const; // Khóa các giá trị literal
// Lấy một union của các giá trị
type Status = (typeof STATUS)[keyof typeof STATUS];
// "pending" | "approved" | "rejected"
function setStatus(status: Status) {
// TypeScript xác thực và tự động hoàn thành!
}
setStatus(STATUS.APPROVED); // ✓
setStatus("pending"); // ✓
setStatus("invalid"); // ✗ Lỗi
Khi Nào Nên Sử Dụng
- Là một giải pháp thay thế cho `enum` với đầu ra JavaScript tốt hơn.
- Tạo cấu hình dựa trên hằng số với các kiểu suy luận.
- Khi bạn muốn cả giá trị thời gian chạy (runtime values) và kiểu thời gian biên dịch (compile-time types).
Tìm hiểu thêm: TypeScript Docs: typeof types
3. Các Phần Tử Tuple Được Gán Nhãn
Các tuple như `[number, number, boolean]` hoạt động, nhưng không rõ ràng mỗi vị trí có ý nghĩa gì. Nó là `[width, height, visible]` hay `[x, y, enabled]`?
Giải Pháp
Gán tên có ý nghĩa cho các vị trí trong tuple, chúng sẽ hiển thị trong gợi ý tự động hoàn thành của trình soạn thảo và trong thông báo lỗi, giúp code dễ đọc và dễ hiểu hơn.
// Trước: không rõ mỗi số có ý nghĩa gì
type Range = [number, number, boolean?];
// Sau: tự tài liệu hóa
type RangeLabeled = [start: number, end: number, inclusive?: boolean];
function createRange([start, end, inclusive = false]: RangeLabeled) {
// Trình soạn thảo của bạn sẽ hiển thị tên tham số!
return { start, end, inclusive };
}
createRange([1, 10, true]); // Rõ ràng ý nghĩa của mỗi đối số
Khi Nào Nên Sử Dụng
- Các tham số hàm dựa trên tuple.
- Giá trị trả về với nhiều phần dữ liệu liên quan.
- Bất kỳ tuple nào mà ý nghĩa của các vị trí không rõ ràng.
Tìm hiểu thêm: TypeScript Docs: tuple types
4. Truy Cập Theo Chỉ Số và Trích Xuất Kiểu Phần Tử
Bạn có một kiểu phức tạp và muốn tham chiếu đến kiểu của chỉ một thuộc tính, hoặc trích xuất những gì bên trong một mảng, mà không cần lặp lại định nghĩa.
Giải Pháp
Sử dụng cú pháp dấu ngoặc vuông (`Type[“property”]`) để truy cập các kiểu thuộc tính, và `[number]` để lấy kiểu phần tử mảng.
type User = {
id: number;
profile: {
name: string;
emails: string[];
};
};
// Truy cập các kiểu thuộc tính lồng nhau
type ProfileType = User["profile"]; // { name: string; emails: string[] }
type NameType = User["profile"]["name"]; // string
// Trích xuất kiểu phần tử mảng
type Email = User["profile"]["emails"][number]; // string
Khi Nào Nên Sử Dụng
- Suy luận kiểu từ các kiểu hiện có (tuân thủ nguyên tắc DRY – Don’t Repeat Yourself).
- Trích xuất kiểu phần tử mảng/tuple.
- Làm việc với các cấu trúc lồng nhau mà không cần định nghĩa lại kiểu.
Tìm hiểu thêm: TypeScript Docs: indexed access types
5. Type Guards Do Người Dùng Định Nghĩa (`arg is T`)
Bạn viết một hàm kiểm tra xem một giá trị có phải là một kiểu cụ thể hay không, nhưng TypeScript không hiểu rằng việc kiểm tra đó thực sự thu hẹp kiểu dữ liệu.
function isPerson(x: unknown) {
return typeof x === "object" && x !== null && "name" in x;
}
function greet(x: unknown) {
if (isPerson(x)) {
x.name; // ✗ Lỗi: TypeScript vẫn nghĩ x là 'unknown'
}
}
Giải Pháp
Sử dụng một mệnh đề kiểu (type predicate) (`arg is Type`) để thông báo cho TypeScript rằng hàm của bạn thực hiện việc kiểm tra kiểu.
type Person = { name: string; age: number };
function isPerson(x: unknown): x is Person {
return (
typeof x === "object" &&
x !== null &&
"name" in x &&
typeof (x as any).name === "string"
);
}
function greet(x: unknown) {
if (isPerson(x)) {
console.log(x.name); // ✓ TypeScript biết x là Person ở đây!
}
}
Khi Nào Nên Sử Dụng
- Xác thực dữ liệu từ API hoặc đầu vào người dùng.
- Các hàm xác thực an toàn kiểu.
- Phân biệt giữa các kiểu trong một union.
Tìm hiểu thêm: TypeScript Docs: type predicates
6. Kiểm Tra Toàn Diện với `never`
Bạn có một kiểu union (ví dụ: các hình dạng hoặc trạng thái khác nhau) và một câu lệnh `switch`. Sau đó, ai đó thêm một biến thể mới vào union nhưng quên xử lý nó trong `switch`. Sẽ không có lỗi nào được đưa ra – nó chỉ âm thầm không hoạt động như mong đợi.
Giải Pháp
Thêm một trường hợp `default` gán giá trị cho kiểu `never`. Nếu tất cả các trường hợp đều được xử lý, trường hợp `default` sẽ không bao giờ được đạt tới. Nếu thiếu một trường hợp, TypeScript sẽ báo lỗi vì giá trị đó không thể gán cho `never`.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
// Nếu tất cả các trường hợp đều được xử lý, dòng này sẽ không được đạt tới
const _exhaustive: never = shape;
throw new Error(`Unhandled shape: ${_exhaustive}`);
}
}
// Sau này, ai đó thêm hình tam giác:
// type Shape = ... | { kind: "triangle"; base: number; height: number };
// ✓ Lỗi TypeScript trong trường hợp default: triangle không thể gán cho never!
Khi Nào Nên Sử Dụng
- Các câu lệnh `switch` trên các union phân biệt.
- Đảm bảo tất cả các biến thể union được xử lý.
- Bắt lỗi khi các kiểu dữ liệu phát triển theo thời gian.
Tìm hiểu thêm: TypeScript Docs: exhaustiveness checking
7. Imports và Exports Chỉ Dành Cho Kiểu (`import type`/`export type`)
Đôi khi bạn nhập các kiểu từ các module khác, nhưng những import đó lại xuất hiện trong mã JavaScript đã biên dịch của bạn mặc dù chúng chỉ được sử dụng cho việc kiểm tra kiểu. Điều này có thể gây ra lỗi phụ thuộc vòng tròn hoặc làm tăng kích thước gói mã (bundle bloat).
Giải Pháp
Sử dụng `import type` để thông báo cho TypeScript: “điều này chỉ cần thiết cho việc kiểm tra kiểu, hãy xóa nó hoàn toàn khỏi JavaScript.”
// Import thông thường - có thể kết thúc trong JS đã biên dịch
import { User } from "./types";
// Import chỉ dành cho kiểu - đảm bảo được xóa khỏi JS
import type { User } from "./types";
// Import kết hợp
import { saveUser, type User } from "./api";
// ^^^^^^^^^ ^^^^^^^^^^^
// giá trị chỉ dành cho kiểu
Khi Nào Nên Sử Dụng
- Ngăn chặn các vấn đề phụ thuộc vòng tròn.
- Giữ cho gói JavaScript của bạn nhỏ hơn.
- Khi sử dụng các công cụ xây dựng yêu cầu import chỉ dành cho kiểu một cách rõ ràng (`isolatedModules`).
- Làm rõ ý định (đây chỉ dành cho kiểu, không phải mã thời gian chạy).
Tìm hiểu thêm: TypeScript Docs: importing types
8. Khai Báo Module Môi Trường cho Tài Nguyên Không Phải Code
Bạn nhập các tệp không phải TypeScript (như hình ảnh, CSS hoặc tệp dữ liệu), nhưng TypeScript không biết chúng nên có kiểu gì.
import logo from "./logo.svg"; // ✗ Lỗi: Không thể tìm thấy module
Giải Pháp
Tạo các khai báo module môi trường (`ambient module declarations`) để thông báo cho TypeScript cách đặt kiểu cho các import này.
// Trong một tệp .d.ts (ví dụ: global.d.ts hoặc declarations.d.ts)
declare module "*.svg" {
const url: string;
export default url;
}
declare module "*.css" {
const classes: { [key: string]: string };
export default classes;
}
// Giờ đây những điều này hoạt động:
import logo from "./logo.svg"; // logo: string
import styles from "./app.css"; // styles: { [key: string]: string }
Khi Nào Nên Sử Dụng
- Đặt kiểu cho các import hình ảnh, font, style.
- Các tệp JSON hoặc dữ liệu không được xử lý bởi công cụ xây dựng của bạn.
- Bất kỳ tài nguyên không phải TypeScript nào được bundler của bạn xử lý.
Tìm hiểu thêm: TypeScript Docs: module declaration templates
9. Toán Tử `satisfies`
Đôi khi bạn muốn TypeScript kiểm tra rằng một đối tượng phù hợp với một kiểu, nhưng bạn cũng muốn TypeScript nhớ các giá trị cụ thể mà bạn đã sử dụng (chứ không chỉ là chúng là chuỗi hoặc số).
// Không có satisfies - mất thông tin cụ thể
const routes: Record<string, string> = {
home: "/",
profile: "/users/:id",
};
// routes.profile chỉ là 'string', không phải là "/users/:id" cụ thể
Giải Pháp
`satisfies` kiểm tra đối tượng của bạn với một kiểu mà không thay đổi những gì TypeScript nhớ về nó.
const routes = {
home: "/",
profile: "/users/:id",
} satisfies Record<string, `/${string}`>; // Phải là chuỗi bắt đầu bằng "/"
// routes.profile vẫn là literal "/users/:id" - giá trị chính xác được bảo toàn!
Khi Nào Nên Sử Dụng
- Các đối tượng cấu hình mà bạn muốn cả xác thực VÀ các kiểu giá trị cụ thể.
- Khi bạn cần tự động hoàn thành trên các giá trị chính xác, không chỉ là kiểu chung.
Tìm hiểu thêm: TypeScript Docs: satisfies operator
10. Hàm Khẳng Định (`asserts` và `asserts x is T`)
Đôi khi bạn muốn một hàm ném lỗi nếu một điều kiện không được đáp ứng. Type guards (được đề cập ở trên) chỉ hoạt động trong câu lệnh `if` – chúng không ảnh hưởng đến mã sau khi gọi hàm.
function assertNotNull(x: unknown) {
if (x == null) throw new Error("Value is null!");
}
const data: string | null = getValue();
assertNotNull(data);
// TypeScript vẫn nghĩ data có thể là null ở đây
Giải Pháp
Các hàm khẳng định sử dụng `asserts` để thông báo cho TypeScript: “nếu hàm này trả về (không ném lỗi), thì điều kiện là đúng.”
function assertNotNull<T>(x: T): asserts x is NonNullable<T> {
if (x == null) throw new Error("Value is null!");
}
const data: string | null = getValue();
assertNotNull(data);
// ✓ TypeScript giờ đây biết data chắc chắn là string ở đây!
data.toUpperCase(); // An toàn khi sử dụng
Khi Nào Nên Sử Dụng
- Các hàm xác thực ném lỗi khi thất bại.
- Thực thi các bất biến thời gian chạy.
- Kiểm tra lỗi sớm tại các ranh giới hàm.
Tìm hiểu thêm: TypeScript Docs: assertion functions
11. Kiểu Chuỗi Literal Mẫu cho Các Mẫu Chuỗi
Hãy tưởng tượng bạn có các tên sự kiện như `”user:login”`, `”user:logout”`, `”post:create”`, v.v. Bạn muốn TypeScript tự động hoàn thành chúng và bắt các lỗi chính tả, nhưng có quá nhiều để liệt kê thủ công.
Giải Pháp
Các kiểu chuỗi literal mẫu cho phép bạn mô tả các mẫu chuỗi bằng cú pháp tương tự như chuỗi mẫu của JavaScript.
// Tự động tạo tất cả các kết hợp
type EventName = `${"user" | "post"}:${"create" | "delete"}`;
// Kết quả: "user:create" | "user:delete" | "post:create" | "post:delete"
function trackEvent(event: EventName) {
// TypeScript sẽ tự động hoàn thành và xác thực tên sự kiện!
}
trackEvent("user:create"); // ✓ OK
trackEvent("user:update"); // ✗ Lỗi - không phải là sự kết hợp hợp lệ
Khi Nào Nên Sử Dụng
- Các tuyến API hoặc tên sự kiện tuân theo một mẫu.
- Tên lớp CSS với tiền tố/hậu tố.
- Bất kỳ định dạng chuỗi có cấu trúc nào (như tên bảng cơ sở dữ liệu, đường dẫn tệp).
Tìm hiểu thêm: TypeScript Docs: template literal types
12. Kiểu Điều Kiện Phân Phối (Distributive Conditional Types)
Bạn muốn lọc hoặc biến đổi một kiểu union (như `string | number | null`) bằng cách áp dụng logic cho từng thành viên.
Giải Pháp
Các kiểu điều kiện tự động phân phối trên các union khi kiểu được kiểm tra là “naked” (không được bọc trong một kiểu khác).
// Loại bỏ null và undefined khỏi một union
type NonNullish<T> = T extends null | undefined ? never : T;
// Điều này phân phối: kiểm tra từng thành viên riêng biệt
type Clean = NonNullish<string | number | null>;
// string | number (null đã được lọc ra)
// Trích xuất chỉ các kiểu hàm
type FunctionsOnly<T> = T extends (...args: any[]) => any ? T : never;
type Fns = FunctionsOnly<string | ((x: number) => void) | boolean>;
// (x: number) => void
Khi Nào Nên Sử Dụng
- Lọc các kiểu union.
- Xây dựng các kiểu tiện ích như `Exclude`, `Extract`.
- Biến đổi từng thành viên của một union theo cách khác nhau.
Tìm hiểu thêm: TypeScript Docs: distributive conditional types
13. `infer` để Bắt Giữ Các Kiểu Bên Trong Điều Kiện
Bạn cần trích xuất một phần của một kiểu phức tạp (chẳng hạn như “hàm này trả về kiểu gì?” hoặc “bên trong mảng này có gì?”).
Giải Pháp
Sử dụng `infer` để tạo một biến kiểu bắt giữ một phần của kiểu bạn đang kiểm tra.
// Trích xuất kiểu trả về của một hàm
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type MyFunc = (x: number) => string;
type Result = ReturnType<MyFunc>; // string
// Trích xuất kiểu phần tử mảng
type ElementType<T> = T extends (infer E)[] ? E : never;
type Numbers = ElementType<number[]>; // number
type Mixed = ElementType<(string | boolean)[]>; // string | boolean
Khi Nào Nên Sử Dụng
- Trích xuất tham số hoặc kiểu trả về từ các hàm.
- Lấy kiểu phần tử từ mảng hoặc tuple.
- Phân tích các kiểu bên trong các literal mẫu hoặc cấu trúc phức tạp.
Tìm hiểu thêm: TypeScript Docs: inferring within conditional types
14. Bộ Điều Chỉnh Kiểu Ánh Xạ (`+readonly`, `-?`, v.v.)
Đôi khi bạn cần lấy một kiểu hiện có và làm cho tất cả các thuộc tính của nó bắt buộc (xóa `?`), hoặc làm cho mọi thứ có thể thay đổi (xóa `readonly`). Việc viết lại thủ công từng thuộc tính là tẻ nhạt và dễ gây lỗi.
Giải Pháp
Các kiểu ánh xạ (mapped types) có thể thêm (`+`) hoặc xóa (`-`) các bộ điều chỉnh `readonly` và optional (`?`).
// Xóa readonly khỏi tất cả các thuộc tính
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
// Xóa optional (?) khỏi tất cả các thuộc tính
type RequiredProps<T> = {
[K in keyof T]-?: T[K];
};
type Config = Readonly<{ port?: number; host?: string }>;
type EditableConfig = Mutable<RequiredProps<Config>>; // { port: number; host: string }
Khi Nào Nên Sử Dụng
- Tạo các phiên bản có thể chỉnh sửa của các kiểu `readonly`.
- Làm cho tất cả các thuộc tính bắt buộc cho các hàm xác thực.
- Xây dựng các kiểu tiện ích biến đổi các bộ điều chỉnh thuộc tính.
Tìm hiểu thêm: TypeScript Docs: mapping modifiers
15. Ánh Xạ Lại Khóa Trong Kiểu Ánh Xạ (`as`)
Bạn muốn biến đổi một kiểu đối tượng bằng cách thay đổi tên thuộc tính (như loại bỏ tiền tố, hoặc lọc ra các thuộc tính nhất định), nhưng các kiểu ánh xạ thông thường giữ nguyên các khóa.
Giải Pháp
Sử dụng `as` bên trong một kiểu ánh xạ để biến đổi các khóa khi bạn lặp qua chúng.
// Loại bỏ các thuộc tính riêng tư (những thuộc tính bắt đầu bằng _)
type RemovePrivate<T> = {
[K in keyof T as K extends `_${string}` ? never : K]: T[K];
};
type WithPrivate = { name: string; _secret: number };
type Public = RemovePrivate<WithPrivate>; // { name: string }
// Thêm tiền tố vào tất cả các khóa
type Prefixed<T> = {
[K in keyof T as `app_${string & K}`]: T[K];
};
type Original = { id: number; name: string };
type Result = Prefixed<Original>; // { app_id: number; app_name: string }
Khi Nào Nên Sử Dụng
- Tạo các phiên bản công khai của các kiểu bằng cách lọc ra các thuộc tính nội bộ.
- Chuyển đổi giữa các quy ước đặt tên (camelCase sang snake_case).
- Lọc các kiểu đối tượng dựa trên các mẫu khóa.
Tìm hiểu thêm: TypeScript Docs: key remapping in mapped types
16. Tham Số Kiểu `const`
Khi bạn truyền một mảng cho một hàm generic, TypeScript thường “mở rộng” nó thành một kiểu mảng chung, làm mất thông tin về các giá trị cụ thể.
function identity<T>(value: T) {
return value;
}
const pair = identity([1, 2]); // Kiểu là number[], không phải [1, 2]
Giải Pháp
Thêm `const` trước tham số kiểu để thông báo cho TypeScript: “giữ cho kiểu này càng cụ thể càng tốt.”
function identity<const T>(value: T) {
return value;
}
const pair = identity([1, 2]); // Kiểu là [1, 2] - tuple chính xác được bảo toàn!
Khi Nào Nên Sử Dụng
- Các hàm nên bảo toàn cấu trúc mảng/tuple chính xác.
- Các hàm xây dựng nơi bạn muốn theo dõi các giá trị literal thông qua các biến đổi.
Tìm hiểu thêm: TypeScript Docs: const type parameters
17. Kiểu Tuple Biến Đổi (Variadic Tuple Types) và Spread
Làm thế nào để bạn đặt kiểu cho một hàm cần chấp nhận số lượng đối số khác nhau trong khi vẫn theo dõi kiểu của từng đối số?
Giải Pháp
Các tuple biến đổi cho phép bạn làm việc với danh sách các kiểu có thể mở rộng hoặc thu hẹp. Hãy nghĩ về `…` như “trải rộng danh sách các kiểu này ở đây.”
// Kiểu thêm một phần tử vào cuối một tuple
type Push<T extends unknown[], U> = [...T, U];
type Result = Push<[string, number], boolean>; // [string, number, boolean]
// Ví dụ thực tế: Đặt kiểu cho một hàm bao bọc
function logged<Args extends unknown[], Return>(
fn: (...args: Args) => Return,
): (...args: Args) => Return {
return (...args) => {
console.log("Calling with:", args);
return fn(...args);
};
}
Khi Nào Nên Sử Dụng
- Bao bọc các hàm trong khi bảo toàn các kiểu đối số chính xác của chúng.
- Tạo các tiện ích kết hợp hàm an toàn kiểu.
- Xây dựng các kiểu thao tác tuple.
Tìm hiểu thêm: TypeScript Docs: tuple types
18. Tham Số `this` Trong Hàm
Khi một hàm sử dụng `this`, TypeScript không biết `this` nên có kiểu gì. Điều này gây ra vấn đề với các phương thức, callback và bộ xử lý sự kiện.
function setName(name: string) {
this.name = name; // ✗ Lỗi: 'this' có kiểu 'any'
}
Giải Pháp
Thêm một tham số `this` rõ ràng (không được tính là một tham số thực sự khi chạy) để đặt kiểu cho `this`.
interface Model {
name: string;
setName(this: Model, newName: string): void;
}
const model: Model = {
name: "Initial",
setName(this: Model, newName: string) {
this.name = newName; // ✓ TypeScript biết 'this' là gì!
},
};
model.setName("Updated"); // Hoạt động
const fn = model.setName;
fn("Test"); // ✗ Lỗi: ngữ cảnh 'this' không đúng
Khi Nào Nên Sử Dụng
- Các phương thức dựa vào `this`.
- Các callback của bộ xử lý sự kiện.
- Các hàm được thiết kế để được gọi bằng `.call()` hoặc `.apply()`.
Tìm hiểu thêm: TypeScript Docs: this parameters
19. `unique symbol` cho Kiểu Định Danh Giống Danh Nghĩa (Nominal-like Typing)
TypeScript sử dụng “kiểu cấu trúc” (structural typing) – hai kiểu có cấu trúc giống nhau được coi là giống nhau. Đôi khi bạn muốn các kiểu giống hệt về cấu trúc nhưng khác nhau về mặt logic (như UserID và ProductID, cả hai đều là chuỗi).
type UserId = string;
type ProductId = string;
function getUser(id: UserId) {
/* ... */
}
const productId: ProductId = "prod-123";
getUser(productId); // ✗ Chúng ta muốn đây là một lỗi, nhưng nó không phải!
Giải Pháp
Sử dụng `unique symbol` để tạo một “brand” (nhãn hiệu) làm cho các kiểu không tương thích ngay cả khi cấu trúc của chúng giống hệt nhau.
declare const USER_ID: unique symbol;
type UserId = string & { [USER_ID]: true };
declare const PRODUCT_ID: unique symbol;
type ProductId = string & { [PRODUCT_ID]: true };
function getUser(id: UserId) {
/* ... */
}
const productId = "prod-123" as ProductId;
getUser(productId); // ✓ Giờ đây đây LÀ một lỗi!
Khi Nào Nên Sử Dụng
- Ngăn chặn việc nhầm lẫn ID của các loại khác nhau (ID người dùng, ID đơn hàng, v.v.).
- Tạo các kiểu “danh nghĩa” (nominal types) không thể vô tình thay thế.
- Các khóa an toàn kiểu cho các registry hoặc dependency injection.
Tìm hiểu thêm: TypeScript Docs: unique symbol
20. Mở Rộng Module và Hợp Nhất Khai Báo (Module Augmentation and Declaration Merging)
Bạn đang sử dụng một thư viện của bên thứ ba và cần thêm các thuộc tính vào các kiểu của nó (chẳng hạn như thêm các tùy chọn cấu hình tùy chỉnh), nhưng bạn không thể chỉnh sửa mã của thư viện.
Giải Pháp
Sử dụng tính năng mở rộng module để thêm vào các interface hoặc module hiện có từ bên ngoài.
// Trong tệp .d.ts của riêng bạn
declare module "express" {
// Thêm vào interface Request của Express
interface Request {
user?: { id: string; name: string };
}
}
// Giờ đây TypeScript biết về req.user trong các bộ xử lý Express của bạn!
Khi Nào Nên Sử Dụng
- Mở rộng các kiểu thư viện với các thuộc tính tùy chỉnh.
- Thêm kiểu cho các tính năng thư viện chưa được đặt kiểu đầy đủ.
- Các hệ thống plugin nơi bạn đang đăng ký các khả năng mới.
Tìm hiểu thêm: TypeScript Docs: module augmentation
21. Chữ Ký Constructor và Các Kiểu “Có Thể Khởi Tạo” Trừu Tượng
Bạn muốn viết một hàm chấp nhận một lớp (chứ không phải một thể hiện) và tạo các thể hiện của lớp đó. Làm thế nào để bạn đặt kiểu cho “thứ gì đó có thể được xây dựng bằng `new`”?
function createInstance(SomeClass: ???) {
return new SomeClass();
}
Giải Pháp
Sử dụng một chữ ký constructor: `new (…args: any[]) => T` để mô tả một thứ có thể được xây dựng.
// Kiểu mô tả một constructor
type Constructor<T = unknown, Args extends unknown[] = any[]> = new (
...args: Args
) => T;
function createInstance<T>(Ctor: Constructor<T>): T {
return new Ctor();
}
class User {
name = "Unknown";
}
const user = createInstance(User); // user: User ✓
// Phức tạp hơn: factory với các đối số constructor cụ thể
function createPair<T>(
Ctor: Constructor<T, [string, number]>,
name: string,
age: number,
): T {
return new Ctor(name, age);
}
Khi Nào Nên Sử Dụng
- Các framework dependency injection.
- Các hàm factory tạo thể hiện.
- Mã generic làm việc với các lớp dưới dạng giá trị.
- Các tiện ích kiểm thử mô phỏng constructor.
Tìm hiểu thêm: TypeScript Docs: constructor signatures in interfaces
Kết Luận
TypeScript không chỉ là một lớp tĩnh đơn giản trên JavaScript; đó là một hệ sinh thái mạnh mẽ với vô số tính năng được thiết kế để nâng cao năng suất và chất lượng mã. Việc nắm vững những tính năng nâng cao này sẽ giúp bạn giải quyết các vấn đề phức tạp hơn, viết code an toàn hơn và tận dụng tối đa tiềm năng của ngôn ngữ này. Hãy bắt đầu áp dụng chúng vào các dự án của bạn để thấy sự khác biệt!
Bạn đã sẵn sàng đưa kỹ năng TypeScript của mình lên một tầm cao mới chưa?



