Sử dụng TypeScript Mapped một cách chuyện nghiệp 😊 (Series: Bón hành TypeScript - PHẦN 5 - Song ngữ: VN - EN - JP)
Chào mừng bạn đến với loạt bài Làm chủ TypeScript. Được nằm trong Series BÓN HÀNH TYPESCRIPT, những bài viết này sẽ giới thiệu về kiến thức và kỹ thuật cốt lõi của TypeScript dưới dạng Animations sinh động.
OK GÉT GÔ
Vấn đề
Bạn đã sử dụng Partial, Required, Readonly, and Pick utility types chưa?
Nếu bạn muốn làm chủ chúng một cách thuần thục và tạo ra các utility types
cho riêng mình thì đừng bỏ qua nội dung được đề cập trong bài viết này.
Tạo một type User
là một kịch bản phổ biến trong công việc hàng ngày. Ở đây, chúng ta có thể sử dụng TypeScript để xác định loại User trong đó tất cả các khóa được yêu cầu.
type User = {
name: string;
password: string;
address: string;
phone: string;
};
Thông thường, đối với Type User đã được khai báo, chúng ta chỉ cho phép sửa đổi một số thông tin. Tại thời điểm này, chúng ta có thể xác định một loại UserPartial mới đại diện cho loại đối tượng User cần cập nhật, trong đó tất cả các khóa là tùy chọn.
type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};
Đối với kịch bản xem thông tin user, chúng ta hy vọng rằng tất cả các khóa trong loại đối tượng tương ứng với đối tượng user đều ở chế độ chỉ đọc (Readonly). Đối với yêu cầu này, chúng ta có thể xác định loại User chỉ đọc.
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
};
Xem lại ba Type liên quan đến user đã được xác định, bạn sẽ thấy rằng chúng chứa rất nhiều code trùng lặp.
Vậy làm cách nào để có thể giảm bớt code trùng lặp trong các loại trên? Câu trả lời là bạn có thể sử dụng các Mapped Types, là các Type chung có thể được sử dụng để ánh xạ loại đối tượng ban đầu sang loại đối tượng mới.
Mapped Type
Cú pháp cho các loại ánh xạ như sau:
Trường hợp P in K
tương tự như câu lệnh in
trong JavaScript, được sử dụng để lặp qua tất cả các loại trong loại K và biến loại T, được sử dụng để biểu thị bất kỳ loại nào trong TypeScript.
Bạn cũng có thể sử dụng các cú pháp sửa đổi bổ sung chỉ đọc và dấu chấm hỏi (?) trong quá trình mapping. Các cú pháp sửa đổi tương ứng được thêm vào và loại bỏ bằng cách thêm các tiền tố dấu cộng(+) và dấu trừ(-). Mặc định là sử dụng dấu cộng nếu không thêm tiền tố.
Bây giờ chúng ta có thể tóm tắt cú pháp của các loại Mapping phổ biến.
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
Sau khi xem cú pháp của các loại Mapped Types
, giờ hãy đến một số ví dụ.
Hãy xem cách xác định lại loại UserPartial
bằng cách sử dụng các Mapped Types.
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
Trong đoạn code trên, chúng ta xác định Mapped Types MyPartial
và sau đó sử dụng nó để ánh xạ loại User thành loại UserPartial
. Toán tử keyof được sử dụng để lấy tất cả các khóa của một loại và kiểu trả về của nó là kiểu kết hợp. Biến loại P thay đổi thành một loại khác với mỗi lần duyệt, T[P]
, tương tự như cú pháp truy cập Properties và được sử dụng để lấy loại value tương ứng với một Properties của loại đối tượng.
Hãy xem ảnh minh họa quy trình thực thi hoàn chỉnh của Mapped Types MyPartial
, nếu chưa rõ, bạn có thể xem nhiều lần để hiểu sâu hơn về Mapped Types TypeScript.
TypeScript 4.1 cho phép chúng ta ánh xạ lại các khóa trong các Mapped Types bằng mệnh đề as
. Cú pháp của nó như sau:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// New Syntax!
}
Trong đó loại NewKeyType
phải là một kiểu con của string | number | symbol union type. Sử dụng mệnh đề as
, chúng ta có thể xác định Getters utility type và tạo ra loại Getter tương ứng cho loại đối tượng.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
Trong đoạn code trên, vì loại được trả về bởi keyof T
có thể chứa kiểu ký hiệu (Symbol type) và kiểu Viết hoa chữ cái đầu (Capitalize utility). Nó yêu cầu kiểu được xử lý cần phải là một kiểu con của loại string
, nên cần phải lọc kiểu bằng toán tử &
.
Ngoài ra, trong quá trình Mapping lại các Keys, chúng ta có thể lọc các Keys bằng cách trả về never type
.
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// type KindlessCircle = {
// radius: number;
// };
Sau khi đọc bài viết này, mình chắc rằng bạn đã hiểu chức năng của các mapped types
và cách implement một số utility types
bên trong TypeScript.
Mapping là một trong những kiến thức nền tảng và cốt lõi để bạn có thể tiến xa hơn trên con đường chinh phục những khái niệm nâng cao khác trong Typescript.
English Version
Issue
Have you used Partial, Required, Readonly, and Pick utility types?
If you want to master them and create your own utility types, don't miss the content discussed in this article.
Creating a type User
is a common scenario in daily work. Here, we can use TypeScript to define the User type where all keys are required.
type User = {
name: string;
password: string;
address: string;
phone: string;
};
Typically, for the declared User Type, we only allow modifications to certain information. At this point, we can define a new type UserPartial representing the type of the User object that needs to be updated, where all keys are optional.
type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};
For the scenario of viewing user information, we hope that all keys in the corresponding type object for the user are in readonly mode. For this requirement, we can define a ReadonlyUser type.
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
};
Reviewing the three related User types that have been defined, you will notice that they contain a lot of duplicated code.
So how can we reduce code duplication in these types? The answer is by using Mapped Types, which are general types that can be used to map the original object type to a new object type.
Mapped Type
The syntax for mapped types is as follows:
The case P in K
is similar to the in
operator in JavaScript, used to iterate over all types in the K type and the T type, which represents any type in TypeScript.
You can also use additional modifiers and the optional modifier (?) in the mapping process. The corresponding modifier syntax is added and removed by adding the plus (+) and minus (-) prefixes. By default, the plus sign is used if no prefix is added.
Now let's summarize the syntax of common mapping types.
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
After examining the syntax of Mapped Types, let's move on to some examples.
Let's see how to redefine the UserPartial type using Mapped Types.
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
In the above code snippet, we define the Mapped Type MyPartial
and then use it to map the User type to the UserPartial type. The keyof operator is used to retrieve all the keys of a type, and its return type is a union of string literal types. The variable type P changes to another type with each iteration, T[P]
, similar to the property access syntax, and is used to obtain the value type corresponding to a property of an object type.
Let's take a look at the illustrated process of the complete execution of the Mapped Type MyPartial
. If it's not clear, you can review it multiple times to gain a deeper understanding of Mapped Types in TypeScript.
TypeScript 4.1 allows us to remap keys in Mapped Types using the as
clause. The syntax for it is as follows:
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// New Syntax!
}
In this case, the type NewKeyType
must be a subtype of the string | number | symbol union type. By using the as
clause, we can define the Getters utility type and create the corresponding Getter type for the object type.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
In the above code snippet, because the return type of keyof T
can contain symbol types and capitalized types (Capitalize utility), it requires that the processed type must be a subtype of the string
type, so we need to filter the type using the &
operator.
Furthermore, during the process of mapping the Keys, we can filter the Keys by returning the never
type.
// Remove the 'kind' property
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// type KindlessCircle = {
// radius: number;
// };
After reading this article, I'm sure you understand the functionality of mapped types and how to implement some utility types within TypeScript.
Mapping is one of the fundamental and core knowledge to progress further on the path of mastering other advanced concepts in Typescript.
日本語版
問題
Partial、Required、Readonly、Pickのユーティリティタイプを使用したことがありますか?
これらをマスターして独自のユーティリティタイプを作成したい場合、この記事で議論されている内容を見逃さないでください。
「User」というタイプを作成することは、日常の仕事ではよくあるシナリオです。ここでは、すべてのキーが必須であるUserタイプを定義するためにTypeScriptを使用できます。
type User = {
name: string;
password: string;
address: string;
phone: string;
};
通常、宣言されたUserタイプでは、特定の情報のみの変更を許可します。この時点で、すべてのキーがオプションである、更新が必要なUserオブジェクトの型を表すUserPartialという新しいタイプを定義できます。
type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};
ユーザー情報を表示するシナリオでは、ユーザーオブジェクトに対応する型のすべてのキーが読み取り専用モードになっていることを期待します。この要件に対して、ReadonlyUserというタイプを定義できます。
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
};
定義された3つの関連するUserタイプを見直すと、重複したコードが多いことに気付くでしょう。
これらのタイプでコードの重複を減らすにはどうすればよいでしょうか?答えは、マップされたタイプを使用することです。マップされたタイプは、元のオブジェクトタイプを新しいオブジェクトタイプにマップするために使用できる一般的なタイプです。
マップされたタイプ
マップされたタイプの構文は次のようになります。
ケースP in K
は、JavaScriptのin
演算子に似ており、KタイプとTタイプのすべての型を反復処理するために使用されます。TタイプはTypeScriptの任意の型を表します。
マッピングプロセスでは、追加の修飾子とオプション修飾子(?)を使用することもできます。対応する修飾子の構文は、プラス(+)とマイナス(-)のプレフィックスを追加することで追加および削除されます。プレフィックスが追加されない場合は、デフォルトでプラス記号が使用されます。
さて、一般的なマッピングタイプの構文をまとめましょう。
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
マップされたタイプの構文を調べた後は、いくつかの例に進みましょう。
マップされたタイプを使用してUserPartialタイプを再定義する方法を見てみましょう。
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
上記のコードスニペットでは、マップされたタイプMyPartial
を定義し、それを使用してUserタイプをUserPartialタイプにマッピングしています。keyof演算子は、型のすべてのキーを取得するために使用され、その戻り値の型は文字列リテラル型のユニオンです。変数型Pは各反復処理ごとに別の型に変わり、T[P]
というプロパティアクセス構文と似た形式で使用され、オブジェクトタイプのプロパティに対応する値型を取得するために使用されます。
マップされたタイプMyPartial
の完全な実行の示されたプロセスを見てみましょう。わかりにくい場合は、複数回レビューしてTypeScriptにおけるマップされたタイプのより深い理解を得てください。
TypeScript 4.1では、マップされたタイプでas
節を使用してキーを再マップすることができます。その構文は次のようになります。
type MappedTypeWithNewKeys<T> = {
[K in keyof T as NewKeyType]: T[K]
// ^^^^^^^^^^^^^
// 新しい構文!
}
この場合、タイプNewKeyType
は、string | number | symbolのユニオン型のサブタイプである必要があります。as
節を使用することで、Getterユーティリティタイプを定義し、オブジェクトタイプに対応するGetterタイプを作成できます。
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
上記のコードスニペットでは、keyof T
の戻り値にはシンボル型と大文字化された型(Capitalizeユーティリティ)が含まれるため、処理される型はstring
型のサブタイプである必要があります。そのため、&
演算子を使用して型をフィルタリングする必要があります。
さらに、Keysのマッピングプロセス中にnever
型を返すことで、Keysをフィルタリングすることもできます。
// 'kind'プロパティを削除する
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// type KindlessCircle = {
// radius: number;
// };
この記事を読んだ後は、マップされたタイプの機能とTypeScriptでいくつかのユーティリティタイプを実装する方法を理解しているはずです。
マッピングは、TypeScriptの他の高度な概念をマスターするための基本的で核となる知識の一つです。
Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.
Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.
Momo: NGUYỄN ANH TUẤN - 0374226770
TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)
All rights reserved