Làm chủ các utility type của TypeScript: công cụ mạnh mẽ để biến đổi type, cải thiện khả năng tái sử dụng code và tăng cường an toàn kiểu trong ứng dụng.
Các Utility Type của TypeScript: Công Cụ Tích Hợp để Thao Tác Type
TypeScript là một ngôn ngữ mạnh mẽ mang lại kiểu tĩnh cho JavaScript. Một trong những tính năng chính của nó là khả năng thao tác các kiểu, cho phép các nhà phát triển tạo ra mã nguồn mạnh mẽ và dễ bảo trì hơn. TypeScript cung cấp một bộ các utility type tích hợp sẵn giúp đơn giản hóa các phép biến đổi kiểu phổ biến. Các utility type này là những công cụ vô giá để tăng cường an toàn kiểu, cải thiện khả năng tái sử dụng mã nguồn và hợp lý hóa quy trình phát triển của bạn. Hướng dẫn toàn diện này sẽ khám phá các utility type cần thiết nhất của TypeScript, cung cấp các ví dụ thực tế và thông tin chi tiết hữu ích để giúp bạn làm chủ chúng.
Utility Types của TypeScript là gì?
Utility types là các toán tử kiểu được định nghĩa trước dùng để biến đổi các kiểu hiện có thành các kiểu mới. Chúng được tích hợp sẵn trong ngôn ngữ TypeScript và cung cấp một cách ngắn gọn, tường minh để thực hiện các thao tác kiểu phổ biến. Sử dụng utility types có thể giảm đáng kể mã nguồn lặp lại (boilerplate code) và làm cho các định nghĩa kiểu của bạn trở nên biểu cảm và dễ hiểu hơn.
Hãy coi chúng như những hàm hoạt động trên các kiểu thay vì giá trị. Chúng nhận một kiểu làm đầu vào và trả về một kiểu đã được sửa đổi làm đầu ra. Điều này cho phép bạn tạo ra các mối quan hệ và biến đổi kiểu phức tạp với mã nguồn tối thiểu.
Tại sao nên sử dụng Utility Types?
Có một số lý do thuyết phục để kết hợp utility types vào các dự án TypeScript của bạn:
- Tăng cường An toàn Kiểu: Utility types giúp bạn thực thi các ràng buộc kiểu chặt chẽ hơn, giảm khả năng xảy ra lỗi runtime và cải thiện độ tin cậy tổng thể của mã nguồn.
- Cải thiện Khả năng Tái sử dụng Code: Bằng cách sử dụng utility types, bạn có thể tạo ra các thành phần và hàm generic hoạt động với nhiều loại kiểu khác nhau, thúc đẩy việc tái sử dụng mã nguồn và giảm sự trùng lặp.
- Giảm Boilerplate: Utility types cung cấp một cách ngắn gọn và tường minh để thực hiện các phép biến đổi kiểu phổ biến, giảm lượng mã nguồn lặp lại mà bạn cần phải viết.
- Tăng cường Khả năng Đọc: Utility types làm cho các định nghĩa kiểu của bạn trở nên biểu cảm và dễ hiểu hơn, cải thiện khả năng đọc và bảo trì của mã nguồn.
Các Utility Type Thiết yếu của TypeScript
Hãy cùng khám phá một số utility type được sử dụng phổ biến và hữu ích nhất trong TypeScript. Chúng ta sẽ tìm hiểu về mục đích, cú pháp và cung cấp các ví dụ thực tế để minh họa cách sử dụng chúng.
1. Partial<T>
Utility type Partial<T>
làm cho tất cả các thuộc tính của kiểu T
trở thành tùy chọn (optional). Điều này hữu ích khi bạn muốn tạo một kiểu mới có một vài hoặc tất cả các thuộc tính của một kiểu hiện có, nhưng bạn không muốn yêu cầu tất cả chúng phải có mặt.
Cú pháp:
type Partial<T> = { [P in keyof T]?: T[P]; };
Ví dụ:
interface User {
id: number;
name: string;
email: string;
}
type OptionalUser = Partial<User>; // Tất cả các thuộc tính giờ đây đều là tùy chọn
const partialUser: OptionalUser = {
name: "Alice", // Chỉ cung cấp thuộc tính name
};
Trường hợp sử dụng: Cập nhật một đối tượng chỉ với một số thuộc tính nhất định. Ví dụ, hãy tưởng tượng một biểu mẫu cập nhật hồ sơ người dùng. Bạn không muốn yêu cầu người dùng cập nhật mọi trường cùng một lúc.
2. Required<T>
Utility type Required<T>
làm cho tất cả các thuộc tính của kiểu T
trở thành bắt buộc. Nó trái ngược với Partial<T>
. Điều này hữu ích khi bạn có một kiểu với các thuộc tính tùy chọn và bạn muốn đảm bảo rằng tất cả các thuộc tính đều có mặt.
Cú pháp:
type Required<T> = { [P in keyof T]-?: T[P]; };
Ví dụ:
interface Config {
apiKey?: string;
apiUrl?: string;
}
type CompleteConfig = Required<Config>; // Tất cả các thuộc tính giờ đây đều là bắt buộc
const config: CompleteConfig = {
apiKey: "your-api-key",
apiUrl: "https://example.com/api",
};
Trường hợp sử dụng: Thực thi rằng tất cả các cài đặt cấu hình được cung cấp trước khi khởi động ứng dụng. Điều này có thể giúp ngăn ngừa lỗi runtime do thiếu hoặc không xác định cài đặt.
3. Readonly<T>
Utility type Readonly<T>
làm cho tất cả các thuộc tính của kiểu T
trở thành chỉ đọc (readonly). Điều này ngăn bạn vô tình sửa đổi các thuộc tính của một đối tượng sau khi nó đã được tạo. Điều này thúc đẩy tính bất biến và cải thiện khả năng dự đoán của mã nguồn.
Cú pháp:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
Ví dụ:
interface Product {
id: number;
name: string;
price: number;
}
type ImmutableProduct = Readonly<Product>; // Tất cả các thuộc tính giờ đây đều là chỉ đọc
const product: ImmutableProduct = {
id: 123,
name: "Example Product",
price: 25.99,
};
// product.price = 29.99; // Lỗi: Không thể gán cho 'price' vì nó là một thuộc tính chỉ đọc.
Trường hợp sử dụng: Tạo các cấu trúc dữ liệu bất biến, chẳng hạn như các đối tượng cấu hình hoặc đối tượng truyền dữ liệu (DTOs), không nên được sửa đổi sau khi tạo. Điều này đặc biệt hữu ích trong các mô hình lập trình chức năng.
4. Pick<T, K extends keyof T>
Utility type Pick<T, K extends keyof T>
tạo ra một kiểu mới bằng cách chọn một tập hợp các thuộc tính K
từ kiểu T
. Điều này hữu ích khi bạn chỉ cần một tập hợp con các thuộc tính của một kiểu hiện có.
Cú pháp:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; };
Ví dụ:
interface Employee {
id: number;
name: string;
department: string;
salary: number;
}
type EmployeeNameAndDepartment = Pick<Employee, "name" | "department">; // Chỉ lấy name và department
const employeeInfo: EmployeeNameAndDepartment = {
name: "Bob",
department: "Engineering",
};
Trường hợp sử dụng: Tạo các đối tượng truyền dữ liệu (DTOs) chuyên biệt chỉ chứa dữ liệu cần thiết cho một hoạt động cụ thể. Điều này có thể cải thiện hiệu suất và giảm lượng dữ liệu được truyền qua mạng. Hãy tưởng tượng việc gửi chi tiết người dùng cho client nhưng loại trừ thông tin nhạy cảm như lương. Bạn có thể sử dụng Pick để chỉ gửi `id` và `name`.
5. Omit<T, K extends keyof any>
Utility type Omit<T, K extends keyof any>
tạo ra một kiểu mới bằng cách bỏ qua một tập hợp các thuộc tính K
từ kiểu T
. Điều này trái ngược với Pick<T, K extends keyof T>
và hữu ích khi bạn muốn loại trừ một số thuộc tính nhất định khỏi một kiểu hiện có.
Cú pháp:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Ví dụ:
interface Event {
id: number;
title: string;
description: string;
date: Date;
location: string;
}
type EventSummary = Omit<Event, "description" | "location">; // Bỏ qua description và location
const eventPreview: EventSummary = {
id: 1,
title: "Conference",
date: new Date(),
};
Trường hợp sử dụng: Tạo các phiên bản đơn giản hóa của các mô hình dữ liệu cho các mục đích cụ thể, chẳng hạn như hiển thị tóm tắt của một sự kiện mà không bao gồm mô tả đầy đủ và vị trí. Điều này cũng có thể được sử dụng để loại bỏ các trường nhạy cảm trước khi gửi dữ liệu cho client.
6. Exclude<T, U>
Utility type Exclude<T, U>
tạo ra một kiểu mới bằng cách loại trừ khỏi T
tất cả các kiểu có thể gán cho U
. Điều này hữu ích khi bạn muốn loại bỏ một số kiểu nhất định khỏi một union type.
Cú pháp:
type Exclude<T, U> = T extends U ? never : T;
Ví dụ:
type AllowedFileTypes = "image" | "video" | "audio" | "document";
type MediaFileTypes = "image" | "video" | "audio";
type DocumentFileTypes = Exclude<AllowedFileTypes, MediaFileTypes>; // "document"
const fileType: DocumentFileTypes = "document";
Trường hợp sử dụng: Lọc một union type để loại bỏ các kiểu cụ thể không liên quan trong một ngữ cảnh nhất định. Ví dụ, bạn có thể muốn loại trừ một số loại tệp nhất định khỏi danh sách các loại tệp được phép.
7. Extract<T, U>
Utility type Extract<T, U>
tạo ra một kiểu mới bằng cách trích xuất từ T
tất cả các kiểu có thể gán cho U
. Điều này trái ngược với Exclude<T, U>
và hữu ích khi bạn muốn chọn các kiểu cụ thể từ một union type.
Cú pháp:
type Extract<T, U> = T extends U ? T : never;
Ví dụ:
type InputTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = string | number | boolean;
type NonNullablePrimitives = Extract<InputTypes, PrimitiveTypes>; // string | number | boolean
const value: NonNullablePrimitives = "hello";
Trường hợp sử dụng: Chọn các kiểu cụ thể từ một union type dựa trên các tiêu chí nhất định. Ví dụ, bạn có thể muốn trích xuất tất cả các kiểu nguyên thủy từ một union type bao gồm cả kiểu nguyên thủy và kiểu đối tượng.
8. NonNullable<T>
Utility type NonNullable<T>
tạo ra một kiểu mới bằng cách loại trừ null
và undefined
khỏi kiểu T
. Điều này hữu ích khi bạn muốn đảm bảo rằng một kiểu không thể là null
hoặc undefined
.
Cú pháp:
type NonNullable<T> = T extends null | undefined ? never : T;
Ví dụ:
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
const message: DefinitelyString = "Hello, world!";
Trường hợp sử dụng: Thực thi rằng một giá trị không phải là null
hoặc undefined
trước khi thực hiện một thao tác trên nó. Điều này có thể giúp ngăn ngừa lỗi runtime gây ra bởi các giá trị null hoặc undefined không mong muốn. Hãy xem xét một kịch bản mà bạn cần xử lý địa chỉ của người dùng, và điều quan trọng là địa chỉ không được null trước bất kỳ thao tác nào.
9. ReturnType<T extends (...args: any) => any>
Utility type ReturnType<T extends (...args: any) => any>
trích xuất kiểu trả về của một kiểu hàm T
. Điều này hữu ích khi bạn muốn biết kiểu của giá trị mà một hàm trả về.
Cú pháp:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Ví dụ:
function fetchData(url: string): Promise<{ data: any }> {
return fetch(url).then(response => response.json());
}
type FetchDataReturnType = ReturnType<typeof fetchData>; // Promise<{ data: any }>
async function processData(data: FetchDataReturnType) {
// ...
}
Trường hợp sử dụng: Xác định kiểu của giá trị được trả về bởi một hàm, đặc biệt khi xử lý các hoạt động bất đồng bộ hoặc các chữ ký hàm phức tạp. Điều này cho phép bạn đảm bảo rằng bạn đang xử lý giá trị trả về một cách chính xác.
10. Parameters<T extends (...args: any) => any>
Utility type Parameters<T extends (...args: any) => any>
trích xuất các kiểu tham số của một kiểu hàm T
dưới dạng một tuple. Điều này hữu ích khi bạn muốn biết các kiểu của các đối số mà một hàm chấp nhận.
Cú pháp:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
Ví dụ:
function createUser(name: string, age: number, email: string): void {
// ...
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, string]
function logUser(...args: CreateUserParams) {
console.log("Creating user with:", args);
}
Trường hợp sử dụng: Xác định các kiểu của các đối số mà một hàm chấp nhận, điều này có thể hữu ích để tạo các hàm generic hoặc decorator cần hoạt động với các hàm có chữ ký khác nhau. Nó giúp đảm bảo an toàn kiểu khi truyền các đối số cho một hàm một cách động.
11. ConstructorParameters<T extends abstract new (...args: any) => any>
Utility type ConstructorParameters<T extends abstract new (...args: any) => any>
trích xuất các kiểu tham số của một kiểu hàm khởi tạo T
dưới dạng một tuple. Điều này hữu ích khi bạn muốn biết các kiểu của các đối số mà một hàm khởi tạo chấp nhận.
Cú pháp:
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
Ví dụ:
class Logger {
constructor(public prefix: string, public enabled: boolean) {}
log(message: string) {
if (this.enabled) {
console.log(`${this.prefix}: ${message}`);
}
}
}
type LoggerConstructorParams = ConstructorParameters<typeof Logger>; // [string, boolean]
function createLogger(...args: LoggerConstructorParams) {
return new Logger(...args);
}
Trường hợp sử dụng: Tương tự như Parameters
, nhưng dành riêng cho các hàm khởi tạo (constructor). Nó hữu ích khi tạo các factory hoặc hệ thống dependency injection nơi bạn cần khởi tạo các lớp một cách động với các chữ ký hàm khởi tạo khác nhau.
12. InstanceType<T extends abstract new (...args: any) => any>
Utility type InstanceType<T extends abstract new (...args: any) => any>
trích xuất kiểu thực thể của một kiểu hàm khởi tạo T
. Điều này hữu ích khi bạn muốn biết kiểu của đối tượng mà một hàm khởi tạo tạo ra.
Cú pháp:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
Ví dụ:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
type GreeterInstance = InstanceType<typeof Greeter>; // Greeter
const myGreeter: GreeterInstance = new Greeter("World");
console.log(myGreeter.greet());
Trường hợp sử dụng: Xác định kiểu của đối tượng được tạo bởi một hàm khởi tạo, điều này hữu ích khi làm việc với tính kế thừa hoặc đa hình. Nó cung cấp một cách an toàn về kiểu để tham chiếu đến một thực thể của một lớp.
13. Record<K extends keyof any, T>
Utility type Record<K extends keyof any, T>
xây dựng một kiểu đối tượng có các khóa thuộc tính là K
và các giá trị thuộc tính là T
. Điều này hữu ích để tạo các kiểu giống như từ điển (dictionary) nơi bạn biết trước các khóa.
Cú pháp:
type Record<K extends keyof any, T> = { [P in K]: T; };
Ví dụ:
type CountryCode = "US" | "CA" | "GB" | "DE";
type CurrencyMap = Record<CountryCode, string>; // { US: string; CA: string; GB: string; DE: string; }
const currencies: CurrencyMap = {
US: "USD",
CA: "CAD",
GB: "GBP",
DE: "EUR",
};
Trường hợp sử dụng: Tạo các đối tượng giống như từ điển nơi bạn có một tập hợp các khóa cố định và muốn đảm bảo rằng tất cả các khóa đều có giá trị thuộc một kiểu cụ thể. Điều này phổ biến khi làm việc với các tệp cấu hình, ánh xạ dữ liệu hoặc bảng tra cứu.
Tạo Utility Type Tùy chỉnh
Mặc dù các utility type tích hợp sẵn của TypeScript rất mạnh mẽ, bạn cũng có thể tạo ra các utility type tùy chỉnh của riêng mình để giải quyết các nhu cầu cụ thể trong dự án. Điều này cho phép bạn đóng gói các phép biến đổi kiểu phức tạp và tái sử dụng chúng trong toàn bộ cơ sở mã của bạn.
Ví dụ:
// Một utility type để lấy các khóa của một đối tượng có một kiểu cụ thể
type KeysOfType<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
interface Person {
name: string;
age: number;
address: string;
phoneNumber: number;
}
type StringKeys = KeysOfType<Person, string>; // "name" | "address"
Các Thực hành Tốt nhất khi Sử dụng Utility Types
- Sử dụng tên mô tả: Đặt tên có ý nghĩa cho các utility type của bạn để chỉ rõ mục đích của chúng. Điều này cải thiện khả năng đọc và bảo trì của mã nguồn.
- Ghi chú tài liệu cho utility types của bạn: Thêm nhận xét để giải thích các utility type của bạn làm gì và nên được sử dụng như thế nào. Điều này giúp các nhà phát triển khác hiểu mã nguồn của bạn và sử dụng nó một cách chính xác.
- Giữ cho nó đơn giản: Tránh tạo ra các utility type quá phức tạp và khó hiểu. Hãy chia các phép biến đổi phức tạp thành các utility type nhỏ hơn, dễ quản lý hơn.
- Kiểm thử utility types của bạn: Viết các unit test để đảm bảo rằng các utility type của bạn hoạt động chính xác. Điều này giúp ngăn ngừa các lỗi không mong muốn và đảm bảo rằng các kiểu của bạn đang hoạt động như mong đợi.
- Xem xét hiệu suất: Mặc dù utility types thường không có tác động đáng kể đến hiệu suất, hãy chú ý đến sự phức tạp của các phép biến đổi kiểu của bạn, đặc biệt là trong các dự án lớn.
Kết luận
Các utility type của TypeScript là những công cụ mạnh mẽ có thể cải thiện đáng kể độ an toàn kiểu, khả năng tái sử dụng và khả năng bảo trì của mã nguồn. Bằng cách làm chủ các utility type này, bạn có thể viết các ứng dụng TypeScript mạnh mẽ và biểu cảm hơn. Hướng dẫn này đã trình bày các utility type cần thiết nhất của TypeScript, cung cấp các ví dụ thực tế và thông tin chi tiết hữu ích để giúp bạn kết hợp chúng vào các dự án của mình.
Hãy nhớ thử nghiệm với các utility type này và khám phá cách chúng có thể được sử dụng để giải quyết các vấn đề cụ thể trong mã nguồn của riêng bạn. Khi bạn trở nên quen thuộc hơn với chúng, bạn sẽ thấy mình sử dụng chúng ngày càng nhiều để tạo ra các ứng dụng TypeScript sạch hơn, dễ bảo trì hơn và an toàn hơn về kiểu. Cho dù bạn đang xây dựng ứng dụng web, ứng dụng phía máy chủ hay bất cứ thứ gì khác, utility types đều cung cấp một bộ công cụ có giá trị để cải thiện quy trình phát triển và chất lượng mã nguồn của bạn. Bằng cách tận dụng các công cụ thao tác kiểu tích hợp này, bạn có thể khai thác toàn bộ tiềm năng của TypeScript và viết mã nguồn vừa biểu cảm vừa mạnh mẽ.