Tiếng Việt

Tìm hiểu cách tận dụng mapped types của TypeScript để biến đổi linh hoạt hình dạng đối tượng, tạo ra mã nguồn mạnh mẽ và dễ bảo trì cho các ứng dụng toàn cầu.

TypeScript Mapped Types cho việc Biến đổi Đối tượng Động: Hướng dẫn Toàn diện

TypeScript, với sự nhấn mạnh vào kiểu tĩnh, trao quyền cho các nhà phát triển viết mã đáng tin cậy và dễ bảo trì hơn. Một tính năng quan trọng đóng góp đáng kể vào điều này là mapped types. Hướng dẫn này đi sâu vào thế giới của TypeScript mapped types, cung cấp một sự hiểu biết toàn diện về chức năng, lợi ích và các ứng dụng thực tế của chúng, đặc biệt là trong bối cảnh phát triển các giải pháp phần mềm toàn cầu.

Hiểu các Khái niệm Cốt lõi

Về cơ bản, một mapped type cho phép bạn tạo một kiểu mới dựa trên các thuộc tính của một kiểu hiện có. Bạn định nghĩa một kiểu mới bằng cách lặp qua các khóa của một kiểu khác và áp dụng các biến đổi cho các giá trị. Điều này cực kỳ hữu ích cho các tình huống mà bạn cần sửa đổi động cấu trúc của các đối tượng, chẳng hạn như thay đổi kiểu dữ liệu của thuộc tính, làm cho các thuộc tính trở thành tùy chọn, hoặc thêm các thuộc tin mới dựa trên các thuộc tính hiện có.

Hãy bắt đầu với những điều cơ bản. Xem xét một interface đơn giản:

interface Person {
  name: string;
  age: number;
  email: string;
}

Bây giờ, hãy định nghĩa một mapped type làm cho tất cả các thuộc tính của Person trở thành tùy chọn (optional):

type OptionalPerson = { 
  [K in keyof Person]?: Person[K];
};

Trong ví dụ này:

Kiểu OptionalPerson kết quả trông như thế này:

{
  name?: string;
  age?: number;
  email?: string;
}

Điều này chứng tỏ sức mạnh của mapped types trong việc sửa đổi động các kiểu hiện có.

Cú pháp và Cấu trúc của Mapped Types

Cú pháp của một mapped type khá đặc trưng và tuân theo cấu trúc chung này:

type NewType = { 
  [Key in KeysType]: ValueType;
};

Hãy phân tích từng thành phần:

Ví dụ: Biến đổi Kiểu Thuộc tính

Hãy tưởng tượng bạn cần chuyển đổi tất cả các thuộc tính số của một đối tượng thành chuỗi. Đây là cách bạn có thể làm điều đó bằng cách sử dụng một mapped type:

interface Product {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

type StringifiedProduct = {
  [K in keyof Product]: Product[K] extends number ? string : Product[K];
};

Trong trường hợp này, chúng ta đang:

Kiểu StringifiedProduct kết quả sẽ là:

{
  id: string;
  name: string;
  price: string;
  quantity: string;
}

Các Tính năng và Kỹ thuật Chính

1. Sử dụng keyof và Index Signatures

Như đã trình bày trước đây, keyof là một công cụ cơ bản để làm việc với mapped types. Nó cho phép bạn lặp qua các khóa của một kiểu. Index signatures cung cấp một cách để định nghĩa kiểu của các thuộc tính khi bạn không biết trước các khóa, nhưng bạn vẫn muốn biến đổi chúng.

Ví dụ: Biến đổi tất cả các thuộc tính dựa trên một index signature

interface StringMap {
  [key: string]: number;
}

type StringMapToString = {
  [K in keyof StringMap]: string;
};

Ở đây, tất cả các giá trị số trong StringMap được chuyển đổi thành chuỗi trong kiểu mới.

2. Kiểu Điều kiện trong Mapped Types

Kiểu điều kiện là một tính năng mạnh mẽ của TypeScript cho phép bạn thể hiện các mối quan hệ kiểu dựa trên các điều kiện. Khi kết hợp với mapped types, chúng cho phép các phép biến đổi rất phức tạp.

Ví dụ: Loại bỏ Null và Undefined khỏi một kiểu

type NonNullableProperties = {
  [K in keyof T]: T[K] extends (null | undefined) ? never : T[K];
};

Mapped type này lặp qua tất cả các khóa của kiểu T và sử dụng một kiểu điều kiện để kiểm tra xem giá trị có cho phép null hoặc undefined hay không. Nếu có, kiểu sẽ được đánh giá là never, loại bỏ hiệu quả thuộc tính đó; nếu không, nó sẽ giữ nguyên kiểu ban đầu. Cách tiếp cận này làm cho các kiểu trở nên mạnh mẽ hơn bằng cách loại trừ các giá trị null hoặc undefined có thể gây ra sự cố, cải thiện chất lượng mã và phù hợp với các phương pháp hay nhất để phát triển phần mềm toàn cầu.

3. Các Utility Types cho Hiệu quả

TypeScript cung cấp các utility types tích hợp giúp đơn giản hóa các tác vụ thao tác kiểu phổ biến. Các kiểu này tận dụng mapped types ở phía sau.

Ví dụ: Sử dụng PickOmit

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

type UserSummary = Pick;
// { id: number; name: string; }

type UserWithoutEmail = Omit;
// { id: number; name: string; role: string; }

Các utility types này giúp bạn không phải viết các định nghĩa mapped type lặp đi lặp lại và cải thiện khả năng đọc mã. Chúng đặc biệt hữu ích trong phát triển toàn cầu để quản lý các chế độ xem khác nhau hoặc các cấp độ truy cập dữ liệu dựa trên quyền của người dùng hoặc ngữ cảnh của ứng dụng.

Các Ứng dụng và Ví dụ trong Thực tế

1. Xác thực và Biến đổi Dữ liệu

Mapped types là vô giá để xác thực và biến đổi dữ liệu nhận được từ các nguồn bên ngoài (API, cơ sở dữ liệu, đầu vào của người dùng). Điều này rất quan trọng trong các ứng dụng toàn cầu nơi bạn có thể phải xử lý dữ liệu từ nhiều nguồn khác nhau và cần đảm bảo tính toàn vẹn của dữ liệu. Chúng cho phép bạn định nghĩa các quy tắc cụ thể, chẳng hạn như xác thực kiểu dữ liệu, và tự động sửa đổi cấu trúc dữ liệu dựa trên các quy tắc này.

Ví dụ: Chuyển đổi Phản hồi API

interface ApiResponse {
  userId: string;
  id: string;
  title: string;
  completed: boolean;
}

type CleanedApiResponse = {
  [K in keyof ApiResponse]:
    K extends 'userId' | 'id' ? number :
    K extends 'title' ? string :
    K extends 'completed' ? boolean : any;
};

Ví dụ này biến đổi các thuộc tính userIdid (ban đầu là chuỗi từ API) thành số. Thuộc tính title được định kiểu chính xác thành chuỗi, và completed được giữ là boolean. Điều này đảm bảo tính nhất quán của dữ liệu và tránh các lỗi tiềm ẩn trong quá trình xử lý tiếp theo.

2. Tạo các Props cho Component Tái sử dụng

Trong React và các framework UI khác, mapped types có thể đơn giản hóa việc tạo các props cho component có thể tái sử dụng. Điều này đặc biệt quan trọng khi phát triển các component UI toàn cầu phải thích ứng với các địa phương và giao diện người dùng khác nhau.

Ví dụ: Xử lý Bản địa hóa (Localization)

interface TextProps {
  textId: string;
  defaultText: string;
  locale: string;
}

type LocalizedTextProps = {
  [K in keyof TextProps as `localized-${K}`]: TextProps[K];
};

Trong đoạn mã này, kiểu mới, LocalizedTextProps, thêm tiền tố vào mỗi tên thuộc tính của TextProps. Ví dụ, textId trở thành localized-textId, điều này hữu ích cho việc thiết lập props của component. Mẫu này có thể được sử dụng để tạo ra các props cho phép thay đổi văn bản một cách linh hoạt dựa trên địa phương của người dùng. Điều này rất cần thiết để xây dựng các giao diện người dùng đa ngôn ngữ hoạt động trơn tru trên các khu vực và ngôn ngữ khác nhau, chẳng hạn như trong các ứng dụng thương mại điện tử hoặc các nền tảng mạng xã hội quốc tế. Các props đã được biến đổi cung cấp cho nhà phát triển nhiều quyền kiểm soát hơn đối với việc bản địa hóa và khả năng tạo ra một trải nghiệm người dùng nhất quán trên toàn cầu.

3. Tạo Form Động

Mapped types hữu ích để tạo các trường biểu mẫu một cách linh hoạt dựa trên các mô hình dữ liệu. Trong các ứng dụng toàn cầu, điều này có thể hữu ích để tạo các biểu mẫu thích ứng với các vai trò người dùng hoặc yêu cầu dữ liệu khác nhau.

Ví dụ: Tự động tạo các trường biểu mẫu dựa trên các khóa của đối tượng

interface UserProfile {
  firstName: string;
  lastName: string;
  email: string;
  phoneNumber: string;
}

type FormFields = {
  [K in keyof UserProfile]: {
    label: string;
    type: string;
    required: boolean;
  };
};

Điều này cho phép bạn định nghĩa một cấu trúc biểu mẫu dựa trên các thuộc tính của interface UserProfile. Điều này tránh được việc phải định nghĩa thủ công các trường biểu mẫu, cải thiện tính linh hoạt và khả năng bảo trì của ứng dụng.

Các Kỹ thuật Mapped Type Nâng cao

1. Ánh xạ lại Khóa (Key Remapping)

TypeScript 4.1 đã giới thiệu tính năng ánh xạ lại khóa trong mapped types. Điều này cho phép bạn đổi tên các khóa trong khi biến đổi kiểu. Điều này đặc biệt hữu ích khi điều chỉnh các kiểu cho các yêu cầu API khác nhau hoặc khi bạn muốn tạo ra các tên thuộc tính thân thiện với người dùng hơn.

Ví dụ: Đổi tên thuộc tính

interface Product {
  productId: number;
  productName: string;
  productDescription: string;
  price: number;
}

type ProductDto = {
  [K in keyof Product as `dto_${K}`]: Product[K];
};

Điều này đổi tên mỗi thuộc tính của kiểu Product để bắt đầu bằng dto_. Điều này rất có giá trị khi ánh xạ giữa các mô hình dữ liệu và API sử dụng một quy ước đặt tên khác. Nó quan trọng trong phát triển phần mềm quốc tế nơi các ứng dụng giao tiếp với nhiều hệ thống back-end có thể có các quy ước đặt tên cụ thể, cho phép tích hợp trơn tru.

2. Ánh xạ lại Khóa có Điều kiện

Bạn có thể kết hợp ánh xạ lại khóa với các kiểu điều kiện để thực hiện các phép biến đổi phức tạp hơn, cho phép bạn đổi tên hoặc loại trừ các thuộc tính dựa trên các tiêu chí nhất định. Kỹ thuật này cho phép các phép biến đổi tinh vi.

Ví dụ: Loại trừ các thuộc tính khỏi một DTO


interface Product {
    id: number;
    name: string;
    description: string;
    price: number;
    category: string;
    isActive: boolean;
}

type ProductDto = {
    [K in keyof Product as K extends 'description' | 'isActive' ? never : K]: Product[K]
}

Ở đây, các thuộc tính descriptionisActive được loại bỏ một cách hiệu quả khỏi kiểu ProductDto được tạo ra vì khóa sẽ phân giải thành never nếu thuộc tính là 'description' hoặc 'isActive'. Điều này cho phép tạo ra các đối tượng truyền dữ liệu (DTOs) cụ thể chỉ chứa dữ liệu cần thiết cho các hoạt động khác nhau. Việc truyền dữ liệu có chọn lọc như vậy là rất quan trọng để tối ưu hóa và bảo mật trong một ứng dụng toàn cầu. Các hạn chế truyền dữ liệu đảm bảo rằng chỉ có dữ liệu liên quan được gửi qua mạng, giảm việc sử dụng băng thông và cải thiện trải nghiệm người dùng. Điều này phù hợp với các quy định về quyền riêng tư toàn cầu.

3. Sử dụng Mapped Types với Generics

Mapped types có thể được kết hợp với generics để tạo ra các định nghĩa kiểu rất linh hoạt và có thể tái sử dụng. Điều này cho phép bạn viết mã có thể xử lý nhiều loại kiểu khác nhau, làm tăng đáng kể khả năng tái sử dụng và bảo trì của mã, điều này đặc biệt có giá trị trong các dự án lớn và các nhóm quốc tế.

Ví dụ: Hàm Generic để Biến đổi Thuộc tính Đối tượng


function transformObjectValues(obj: T, transform: (value: T[K]) => U): {
    [P in keyof T]: U;
} {
    const result: any = {};
    for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
            result[key] = transform(obj[key]);
        }
    }
    return result;
}

interface Order {
    id: number;
    items: string[];
    total: number;
}

const order: Order = {
    id: 123,
    items: ['apple', 'banana'],
    total: 5.99,
};

const stringifiedOrder = transformObjectValues(order, (value) => String(value));
// stringifiedOrder: { id: string; items: string; total: string; }

Trong ví dụ này, hàm transformObjectValues sử dụng generics (T, K, và U) để nhận một đối tượng (obj) có kiểu T, và một hàm biến đổi chấp nhận một thuộc tính duy nhất từ T và trả về một giá trị có kiểu U. Sau đó, hàm trả về một đối tượng mới chứa các khóa giống như đối tượng ban đầu nhưng với các giá trị đã được biến đổi sang kiểu U.

Các Phương pháp Tốt nhất và Lưu ý

1. An toàn Kiểu và Khả năng Bảo trì Mã

Một trong những lợi ích lớn nhất của TypeScript và mapped types là tăng cường an toàn kiểu. Bằng cách định nghĩa các kiểu rõ ràng, bạn sẽ phát hiện lỗi sớm hơn trong quá trình phát triển, giảm khả năng xảy ra lỗi lúc chạy. Chúng làm cho mã của bạn dễ hiểu và tái cấu trúc hơn, đặc biệt là trong các dự án lớn. Hơn nữa, việc sử dụng mapped types đảm bảo rằng mã ít bị lỗi hơn khi phần mềm mở rộng quy mô, thích ứng với nhu cầu của hàng triệu người dùng trên toàn cầu.

2. Khả năng đọc và Phong cách Mã

Mặc dù mapped types có thể mạnh mẽ, điều cần thiết là phải viết chúng một cách rõ ràng và dễ đọc. Sử dụng tên biến có ý nghĩa và bình luận mã của bạn để giải thích mục đích của các phép biến đổi phức tạp. Sự rõ ràng của mã đảm bảo rằng các nhà phát triển từ mọi nền tảng đều có thể đọc và hiểu mã. Sự nhất quán trong phong cách, quy ước đặt tên và định dạng làm cho mã dễ tiếp cận hơn và góp phần vào một quy trình phát triển trôi chảy hơn, đặc biệt là trong các nhóm quốc tế nơi các thành viên khác nhau làm việc trên các phần khác nhau của phần mềm.

3. Lạm dụng và Độ phức tạp

Tránh lạm dụng mapped types. Mặc dù chúng mạnh mẽ, chúng có thể làm cho mã kém dễ đọc hơn nếu được sử dụng quá mức hoặc khi có các giải pháp đơn giản hơn. Hãy xem xét liệu một định nghĩa interface đơn giản hoặc một hàm tiện ích đơn giản có thể là một giải pháp thích hợp hơn không. Nếu các kiểu của bạn trở nên quá phức tạp, chúng có thể khó hiểu và bảo trì. Luôn xem xét sự cân bằng giữa an toàn kiểu và khả năng đọc mã. Việc đạt được sự cân bằng này đảm bảo rằng tất cả các thành viên của nhóm quốc tế có thể đọc, hiểu và bảo trì codebase một cách hiệu quả.

4. Hiệu suất

Mapped types chủ yếu ảnh hưởng đến việc kiểm tra kiểu tại thời điểm biên dịch và thường không gây ra chi phí hiệu suất đáng kể lúc chạy. Tuy nhiên, các thao tác kiểu quá phức tạp có thể làm chậm quá trình biên dịch. Giảm thiểu độ phức tạp và xem xét tác động đến thời gian xây dựng, đặc biệt là trong các dự án lớn hoặc cho các nhóm làm việc ở các múi giờ khác nhau và có các ràng buộc tài nguyên khác nhau.

Kết luận

TypeScript mapped types cung cấp một bộ công cụ mạnh mẽ để biến đổi động hình dạng đối tượng. Chúng là vô giá để xây dựng mã an toàn về kiểu, dễ bảo trì và có thể tái sử dụng, đặc biệt là khi xử lý các mô hình dữ liệu phức tạp, tương tác API và phát triển component UI. Bằng cách thành thạo mapped types, bạn có thể viết các ứng dụng mạnh mẽ và dễ thích ứng hơn, tạo ra phần mềm tốt hơn cho thị trường toàn cầu. Đối với các nhóm quốc tế và các dự án toàn cầu, việc sử dụng mapped types mang lại chất lượng mã và khả năng bảo trì mạnh mẽ. Các tính năng được thảo luận ở đây là rất quan trọng để xây dựng phần mềm có khả năng thích ứng và mở rộng, cải thiện khả năng bảo trì mã và tạo ra trải nghiệm tốt hơn cho người dùng trên toàn cầu. Mapped types giúp mã dễ dàng cập nhật hơn khi các tính năng, API hoặc mô hình dữ liệu mới được thêm vào hoặc sửa đổi.