Vượt qua các kiểu cơ bản. Thành thạo các tính năng TypeScript nâng cao như kiểu có điều kiện, literal mẫu và thao tác chuỗi để xây dựng các API cực kỳ mạnh mẽ và an toàn về kiểu. Hướng dẫn toàn diện cho các nhà phát triển toàn cầu.
Khai thác toàn bộ tiềm năng của TypeScript: Đi sâu vào Kiểu có điều kiện, Literal Mẫu và Thao tác Chuỗi Nâng cao
Trong thế giới phát triển phần mềm hiện đại, TypeScript đã phát triển vượt xa vai trò ban đầu của nó như một trình kiểm tra kiểu đơn giản cho JavaScript. Nó đã trở thành một công cụ tinh vi cho những gì có thể được mô tả là lập trình cấp kiểu. Mô hình này cho phép các nhà phát triển viết mã hoạt động trên chính các kiểu, tạo ra các API động, tự tài liệu hóa và cực kỳ an toàn. Trọng tâm của cuộc cách mạng này là ba tính năng mạnh mẽ hoạt động cùng nhau: Kiểu có điều kiện, Kiểu Literal Mẫu và một bộ các Kiểu Thao tác Chuỗi nội tại.
Đối với các nhà phát triển trên toàn cầu đang tìm cách nâng cao kỹ năng TypeScript của mình, việc hiểu các khái niệm này không còn là điều xa xỉ nữa—đó là điều cần thiết để xây dựng các ứng dụng có thể mở rộng và dễ bảo trì. Hướng dẫn này sẽ đưa bạn đi sâu, bắt đầu từ các nguyên tắc cơ bản và xây dựng lên các mẫu phức tạp, thực tế để chứng minh sức mạnh kết hợp của chúng. Cho dù bạn đang xây dựng một hệ thống thiết kế, một trình khách API an toàn về kiểu hay một thư viện xử lý dữ liệu phức tạp, việc thành thạo các tính năng này sẽ thay đổi cách bạn viết TypeScript.
Nền tảng: Kiểu có điều kiện (Toán tử Ba ngôi `extends`)
Cốt lõi, một kiểu có điều kiện cho phép bạn chọn một trong hai kiểu có thể dựa trên việc kiểm tra mối quan hệ kiểu. Nếu bạn quen thuộc với toán tử ba ngôi của JavaScript (điều kiện ? giá trịNếuĐúng : giá trịNếuSai), bạn sẽ thấy cú pháp này ngay lập tức trực quan:
type Kết quả = KiểuNàoĐó extends KiểuKhác ? KiểuĐúng : KiểuSai;
Ở đây, từ khóa extends đóng vai trò là điều kiện của chúng ta. Nó kiểm tra xem KiểuNàoĐó có thể gán cho KiểuKhác hay không. Hãy cùng phân tích nó với một ví dụ đơn giản.
Ví dụ cơ bản: Kiểm tra một kiểu
Hãy tưởng tượng chúng ta muốn tạo một kiểu sẽ phân giải thành true nếu một kiểu T nhất định là một chuỗi, và false nếu không.
type IsString
Sau đó, chúng ta có thể sử dụng kiểu này như sau:
type A = IsString<"hello">; // kiểu A là true
type B = IsString<123>; // kiểu B là false
Đây là khối xây dựng cơ bản. Nhưng sức mạnh thực sự của các kiểu có điều kiện sẽ được giải phóng khi kết hợp với từ khóa infer.
Sức mạnh của `infer`: Trích xuất kiểu từ bên trong
Từ khóa infer là một yếu tố thay đổi cuộc chơi. Nó cho phép bạn khai báo một biến kiểu chung mới trong mệnh đề extends, hiệu quả là nắm bắt một phần của kiểu bạn đang kiểm tra. Hãy coi nó như một khai báo biến cấp kiểu nhận giá trị của nó từ khớp mẫu.
Một ví dụ cổ điển là giải nén kiểu chứa trong Promise.
type UnwrapPromise
Hãy phân tích điều này:
T extends Promise: Điều này kiểm tra xemTcó phải làPromisekhông. Nếu có, TypeScript sẽ cố gắng khớp cấu trúc.infer U: Nếu khớp thành công, TypeScript sẽ nắm bắt kiểu màPromisephân giải thành và đặt nó vào một biến kiểu mới có tên làU.? U : T: Nếu điều kiện đúng (TlàPromise), kiểu kết quả làU(kiểu đã giải nén). Nếu không, kiểu kết quả chỉ là kiểu ban đầuT.
Sử dụng:
type User = { id: number; name: string; };
type UserPromise = Promise
type UnwrappedUser = UnwrapPromise
type UnwrappedNumber = UnwrapPromise
Mẫu này phổ biến đến mức TypeScript bao gồm các kiểu tiện ích tích hợp sẵn như ReturnType, được triển khai bằng nguyên tắc tương tự để trích xuất kiểu trả về của một hàm.
Kiểu có điều kiện phân phối: Làm việc với hợp nhất (Unions)
Một hành vi thú vị và quan trọng của các kiểu có điều kiện là chúng trở nên phân phối khi kiểu được kiểm tra là một tham số kiểu chung "trần". Điều này có nghĩa là nếu bạn truyền một kiểu hợp nhất cho nó, điều kiện sẽ được áp dụng cho từng thành viên của hợp nhất riêng lẻ, và các kết quả sẽ được thu thập lại vào một hợp nhất mới.
Hãy xem xét một kiểu chuyển đổi một kiểu thành một mảng của kiểu đó:
type ToArray
Nếu chúng ta truyền một kiểu hợp nhất cho ToArray:
type StrOrNumArray = ToArray
Kết quả không phải là (string | number)[]. Bởi vì T là một tham số kiểu trần, điều kiện được phân phối:
ToArraytrở thànhstring[]ToArraytrở thànhnumber[]
Kết quả cuối cùng là hợp nhất của các kết quả riêng lẻ này: string[] | number[].
Thuộc tính phân phối này cực kỳ hữu ích để lọc các hợp nhất. Ví dụ, kiểu tiện ích tích hợp sẵn Extract sử dụng điều này để chọn các thành viên từ hợp nhất T có thể gán cho U.
Nếu bạn cần ngăn chặn hành vi phân phối này, bạn có thể bọc tham số kiểu trong một mảng hai chiều ở cả hai phía của mệnh đề extends:
type ToArrayNonDistributive
type StrOrNumArrayUnified = ToArrayNonDistributive
Với nền tảng vững chắc này, hãy cùng khám phá cách chúng ta có thể xây dựng các chuỗi động ở cấp độ kiểu.
Xây dựng Chuỗi Động ở Cấp độ Kiểu: Kiểu Literal Mẫu
Được giới thiệu trong TypeScript 4.1, Kiểu Literal Mẫu cho phép bạn định nghĩa các kiểu có hình dạng giống như chuỗi literal mẫu của JavaScript. Chúng cho phép bạn nối, kết hợp và tạo ra các kiểu chuỗi literal mới từ các kiểu hiện có.
Cú pháp hoàn toàn giống như bạn mong đợi:
type World = "World";
type Greeting = `Hello, ${World}!`; // kiểu Greeting là "Hello, World!"
Điều này có vẻ đơn giản, nhưng sức mạnh của nó nằm ở việc kết hợp nó với hợp nhất và generic.
Hợp nhất và Hoán vị
Khi một kiểu literal mẫu liên quan đến một hợp nhất, nó sẽ mở rộng thành một hợp nhất mới chứa mọi hoán vị chuỗi có thể có. Đây là một cách mạnh mẽ để tạo ra một bộ các hằng số được xác định rõ ràng.
Hãy tưởng tượng việc định nghĩa một bộ các thuộc tính CSS margin:
type Side = "top" | "right" | "bottom" | "left";
type MarginProperty = `margin-${Side}`;
Kiểu kết quả cho MarginProperty là:
"margin-top" | "margin-right" | "margin-bottom" | "margin-left"
Điều này hoàn hảo để tạo các props thành phần hoặc đối số hàm an toàn về kiểu, nơi chỉ cho phép các định dạng chuỗi cụ thể.
Kết hợp với Generic
Các literal mẫu thực sự tỏa sáng khi được sử dụng với generic. Bạn có thể tạo các kiểu nhà máy tạo ra các kiểu chuỗi literal mới dựa trên một số đầu vào.
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Mẫu này là chìa khóa để tạo ra các API động, an toàn về kiểu. Nhưng nếu chúng ta cần sửa đổi cách viết hoa của chuỗi, chẳng hạn như thay đổi "user" thành "User" để nhận "onUserChange"? Đó là lúc các kiểu thao tác chuỗi phát huy tác dụng.
Bộ công cụ: Các Kiểu Thao tác Chuỗi Nội tại
Để làm cho các literal mẫu trở nên mạnh mẽ hơn nữa, TypeScript cung cấp một bộ các kiểu tích hợp sẵn để thao tác các chuỗi literal. Chúng giống như các hàm tiện ích nhưng dành cho hệ thống kiểu.
Các Bộ điều chỉnh Cách viết hoa: `Uppercase`, `Lowercase`, `Capitalize`, `Uncapitalize`
Bốn kiểu này làm chính xác những gì tên của chúng gợi ý:
Uppercase: Chuyển đổi toàn bộ kiểu chuỗi thành chữ hoa.type LOUD = Uppercase<"hello">; // "HELLO"Lowercase: Chuyển đổi toàn bộ kiểu chuỗi thành chữ thường.type quiet = Lowercase<"WORLD">; // "world"Capitalize: Chuyển đổi ký tự đầu tiên của kiểu chuỗi thành chữ hoa.type Proper = Capitalize<"john">; // "John"Uncapitalize: Chuyển đổi ký tự đầu tiên của kiểu chuỗi thành chữ thường.type variable = Uncapitalize<"PersonName">; // "personName"
Hãy xem lại ví dụ trước của chúng ta và cải thiện nó bằng cách sử dụng Capitalize để tạo tên trình xử lý sự kiện thông thường:
type MakeEventListener
type UserListener = MakeEventListener<"user">; // "onUserChange"
type ProductListener = MakeEventListener<"product">; // "onProductChange"
Bây giờ chúng ta có tất cả các mảnh ghép. Hãy xem chúng kết hợp với nhau để giải quyết các vấn đề phức tạp, thực tế như thế nào.
Sự Tổng hợp: Kết hợp Cả Ba cho các Mẫu Nâng cao
Đây là nơi lý thuyết gặp thực tế. Bằng cách đan xen các kiểu có điều kiện, literal mẫu và thao tác chuỗi, chúng ta có thể xây dựng các định nghĩa kiểu cực kỳ tinh vi và an toàn.
Mẫu 1: Trình phát sự kiện được kiểu hóa hoàn toàn an toàn
Mục tiêu: Tạo một lớp EventEmitter chung với các phương thức như on(), off() và emit() được kiểu hóa hoàn toàn an toàn. Điều này có nghĩa là:
- Tên sự kiện được truyền cho các phương thức phải là một sự kiện hợp lệ.
- Dữ liệu tải (payload) được truyền cho
emit()phải khớp với kiểu được định nghĩa cho sự kiện đó. - Hàm gọi lại được truyền cho
on()phải chấp nhận đúng kiểu dữ liệu tải cho sự kiện đó.
Đầu tiên, chúng ta định nghĩa một bản đồ tên sự kiện với các kiểu dữ liệu tải của chúng:
interface EventMap {
"user:created": { userId: number; name: string; };
"user:deleted": { userId: number; };
"product:added": { productId: string; price: number; };
}
Bây giờ, chúng ta có thể xây dựng lớp EventEmitter chung. Chúng ta sẽ sử dụng một tham số chung Events phải kế thừa cấu trúc EventMap của chúng ta.
class TypedEventEmitter
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
// Phương thức `on` sử dụng một generic `K` là một key trong bản đồ Events của chúng ta
on
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]?.push(callback);
}
// Phương thức `emit` đảm bảo dữ liệu tải khớp với kiểu của sự kiện
emit
this.listeners[event]?.forEach(callback => callback(payload));
}
}
Hãy khởi tạo và sử dụng nó:
const appEvents = new TypedEventEmitter
// Điều này an toàn về kiểu. Dữ liệu tải được suy luận chính xác là { userId: number; name: string; }
appEvents.on("user:created", (payload) => {
console.log(`User created: ${payload.name} (ID: ${payload.userId})`);
});
// TypeScript sẽ báo lỗi ở đây vì "user:updated" không phải là một key trong EventMap
// appEvents.on("user:updated", () => {}); // Lỗi!
// TypeScript sẽ báo lỗi ở đây vì dữ liệu tải thiếu thuộc tính 'name'
// appEvents.emit("user:created", { userId: 123 }); // Lỗi!
Mẫu này cung cấp sự an toàn ở cấp độ biên dịch cho những gì theo truyền thống là một phần rất động và dễ xảy ra lỗi của nhiều ứng dụng.
Mẫu 2: Truy cập Đường dẫn An toàn Kiểu cho Đối tượng Lồng nhau
Mục tiêu: Tạo một kiểu tiện ích, PathValue, có thể xác định kiểu của một giá trị trong một đối tượng lồng nhau T bằng cách sử dụng đường dẫn chuỗi ký hiệu chấm P (ví dụ: "user.address.city").
Đây là một mẫu cực kỳ nâng cao, thể hiện các kiểu có điều kiện đệ quy.
Đây là cách triển khai, mà chúng ta sẽ phân tích:
type PathValue
? Key extends keyof T
? PathValue
: never
: P extends keyof T
? T[P]
: never;
Hãy theo dõi logic của nó với một ví dụ: PathValue
- Gọi ban đầu:
Plà"a.b.c". Điều này khớp với literal mẫu`${infer Key}.${infer Rest}`. Keyđược suy luận là"a".Restđược suy luận là"b.c".- Đệ quy lần đầu: Kiểu kiểm tra xem
"a"có phải là một key củaMyObjectkhông. Nếu có, nó sẽ gọi đệ quyPathValue. - Đệ quy lần hai: Bây giờ,
Plà"b.c". Nó lại khớp với literal mẫu. Keyđược suy luận là"b".Restđược suy luận là"c".- Kiểu kiểm tra xem
"b"có phải là một key củaMyObject["a"]và gọi đệ quyPathValue. - Trường hợp cơ sở: Cuối cùng,
Plà"c". Điều này không khớp với`${infer Key}.${infer Rest}`. Logic kiểu sẽ chuyển sang điều kiện thứ hai:P extends keyof T ? T[P] : never. - Kiểu kiểm tra xem
"c"có phải là một key củaMyObject["a"]["b"]không. Nếu có, kết quả làMyObject["a"]["b"]["c"]. Nếu không, nó lànever.
Sử dụng với một hàm trợ giúp:
declare function get
const myObject = {
user: {
name: "Alice",
address: {
city: "Wonderland",
zip: 12345
}
}
};
const city = get(myObject, "user.address.city"); // const city: string
const zip = get(myObject, "user.address.zip"); // const zip: number
const invalid = get(myObject, "user.email"); // const invalid: never
Kiểu mạnh mẽ này ngăn chặn lỗi thời gian chạy do lỗi chính tả trong đường dẫn và cung cấp suy luận kiểu hoàn hảo cho cấu trúc dữ liệu lồng nhau sâu, một thách thức phổ biến trong các ứng dụng toàn cầu xử lý các phản hồi API phức tạp.
Các Phương pháp Hay nhất và Cân nhắc về Hiệu suất
Như với bất kỳ công cụ mạnh mẽ nào, điều quan trọng là phải sử dụng các tính năng này một cách khôn ngoan.
- Ưu tiên Khả năng đọc: Các kiểu phức tạp có thể nhanh chóng trở nên khó đọc. Hãy chia chúng thành các kiểu con nhỏ hơn, được đặt tên tốt. Sử dụng nhận xét để giải thích logic, giống như bạn làm với mã thời gian chạy phức tạp.
- Hiểu Kiểu `never`: Kiểu
neverlà công cụ chính của bạn để xử lý các trạng thái lỗi và lọc các hợp nhất trong các kiểu có điều kiện. Nó đại diện cho một trạng thái không bao giờ nên xảy ra. - Cảnh giác với Giới hạn Đệ quy: TypeScript có giới hạn độ sâu đệ quy cho việc khởi tạo kiểu. Nếu các kiểu của bạn quá lồng nhau hoặc đệ quy vô hạn, trình biên dịch sẽ báo lỗi. Đảm bảo các kiểu đệ quy của bạn có trường hợp cơ sở rõ ràng.
- Theo dõi Hiệu suất IDE: Các kiểu cực kỳ phức tạp đôi khi có thể ảnh hưởng đến hiệu suất của máy chủ ngôn ngữ TypeScript, dẫn đến việc tự động hoàn thành và kiểm tra kiểu chậm hơn trong trình chỉnh sửa của bạn. Nếu bạn gặp phải tình trạng chậm, hãy xem liệu một kiểu phức tạp có thể được đơn giản hóa hoặc chia nhỏ hay không.
- Biết Khi nào nên Dừng lại: Các tính năng này dành để giải quyết các vấn đề phức tạp về an toàn kiểu và trải nghiệm nhà phát triển. Đừng sử dụng chúng để kỹ thuật hóa quá mức các kiểu đơn giản. Mục tiêu là nâng cao sự rõ ràng và an toàn, không phải để thêm độ phức tạp không cần thiết.
Kết luận
Các kiểu có điều kiện, literal mẫu và kiểu thao tác chuỗi không chỉ là các tính năng riêng lẻ; chúng là một hệ thống tích hợp chặt chẽ để thực hiện logic tinh vi ở cấp độ kiểu. Chúng trao quyền cho chúng ta vượt ra ngoài các chú thích đơn giản và xây dựng các hệ thống nhận biết sâu sắc cấu trúc và ràng buộc của chính chúng.
Bằng cách thành thạo bộ ba này, bạn có thể:
- Tạo các API Tự tài liệu hóa: Chính các kiểu trở thành tài liệu, hướng dẫn các nhà phát triển sử dụng chúng một cách chính xác.
- Loại bỏ các Lớp Lỗi Toàn bộ: Lỗi kiểu được bắt ở cấp độ biên dịch, không phải bởi người dùng trong môi trường sản xuất.
- Cải thiện Trải nghiệm Nhà phát triển: Tận hưởng tự động hoàn thành phong phú và thông báo lỗi nội tuyến cho ngay cả những phần động nhất trong cơ sở mã của bạn.
Nắm bắt những khả năng nâng cao này biến TypeScript từ một mạng lưới an toàn thành một đối tác mạnh mẽ trong quá trình phát triển. Nó cho phép chúng ta mã hóa logic kinh doanh và các bất biến phức tạp trực tiếp vào hệ thống kiểu, đảm bảo rằng các ứng dụng của chúng ta mạnh mẽ hơn, dễ bảo trì hơn và có thể mở rộng hơn cho đối tượng toàn cầu.