Một số kỹ thuật TypeScript Nâng Cao
- 21-12-2023
- Toanngo92
- 0 Comments
Mục lục
Union Type là gì?
Union Type trong TypeScript cho phép bạn định nghĩa các kiểu dữ liệu có tính chất “HOẶC”. Đây là cách để chỉ định rằng một biến có thể mang một trong các kiểu dữ liệu đã được định nghĩa. Để tạo Union Type, chúng ta sử dụng ký hiệu “Pipe Symbol” (|
).
Ví dụ, hãy xem xét một hàm listen()
như sau:
function listen(port: unknown) {
if (typeof port === "string") {
port = parseInt(port, 10);
}
server.listen(port);
}
Trong ví dụ này, tham số port được định kiểu là unknown, có nghĩa là nó có thể chứa nhiều loại kiểu dữ liệu khác nhau. Bằng cách sử dụng typeof để kiểm tra nếu port là kiểu “string”, chúng ta thực hiện việc chuyển đổi port thành số nguyên nếu có thể. Điều này cho thấy khả năng của Union Type khi một biến có thể là string hoặc số nguyên, giúp chúng ta xử lý linh hoạt hơn trong các tình huống khác nhau.
typeof là gì?
Operator typeof trong TypeScript được sử dụng để trả về kiểu dữ liệu của một biến hoặc giá trị cụ thể. Kết quả trả về từ typeof
luôn có kiểu là string.
typeof "string"; // string
typeof 123; // number
typeof true; // boolean
typeof {}; // object
typeof []; // object
typeof (() => {}); // function
typeof null; // object
typeof undefined; // undefined
typeof new Date(); // object
typeof String; // function
typeof Boolean; // function
typeof NaN; // number
typeof typeof 123; // string
Khi áp dụng typeof
trong hàm listen()
, tham số port có kiểu unknown, cho phép chúng ta truyền vào một loại kiểu dữ liệu không xác định khi viết mã. Hàm này có thể nhận nhiều loại kiểu dữ liệu như string, number, boolean, mảng, object và function (bao gồm cả undefined và null).
unknown là một kiểu dữ liệu được giới thiệu trong TypeScript 3.0 và khuyến nghị sử dụng unknown thay vì any khi không biết chính xác kiểu dữ liệu của biến.
Tuy nhiên, trong trường hợp hàm listen()
chỉ xử lý hai kiểu dữ liệu string và number, việc sử dụng unknown trở nên không linh hoạt. Đây là lúc Union Type được áp dụng để xác định chính xác các kiểu dữ liệu hợp lệ.
function listen(port: string | number) {
// xử lý
}
listen("3000"); // ok
listen(3000); // ok
listen(true); // TypeError: Kiểu dữ liệu true không thể gán cho tham số kiểu string | number
listen(); // TypeError: Số lượng đối số không hợp lệ, cần 1 đối số
TypeScript sẽ báo lỗi ngay lập tức (Compilation Time Error) nếu bạn truyền vào kiểu dữ liệu không hợp lệ khi gọi hàm listen()
. Mặc dù sau khi biên dịch sang JavaScript, bạn có thể chạy được code nhưng sẽ gặp lỗi tại thời điểm chạy (Runtime Error).
Tạo Union Type cho giá trị trả về của hàm cũng tương tự như tạo Union Type cho tham số. Điều này cho phép tái sử dụng một Union Type bằng cách tạo Type Alias cho nó.
Intersection Type là gì?
Intersection Type trong TypeScript là một loại type cho phép kết hợp nhiều type lại với nhau. Nó tương đương với toán tử “AND” trong logic.
Ví dụ, hãy xem xét hàm merge()
sau:
function merge<T1, T2>(o1: T1, o2: T2): T1 & T2 {
return { ...o1, ...o2 };
}
merge({ foo: "bar" }, { bar: "foo" });
Trong ví dụ này, hàm merge()
nhận vào hai đối tượng và trả về kết quả là kết hợp của hai loại đối tượng đó. Khi gọi hàm merge({ foo: 'bar' }, { bar: 'foo' })
, kết quả trả về sẽ có kiểu dữ liệu { foo: string } & { bar: string }
.
Intersection Type không chỉ được sử dụng trong các hàm thông thường mà không phù hợp với khái niệm OOP, mà còn thường được áp dụng trong việc thiết kế hệ thống type cho các thư viện UI Components.
Ví dụ, trong đoạn code dưới đây, chúng ta có hai loại Props cho Button và Text, mỗi loại có các thuộc tính riêng biệt và cũng chia sẻ một số thuộc tính giống nhau từ type StyleProps:
type StyleProps = {
backgroundColor: string;
color: string;
margin: string;
padding: string;
// ...
}
type ButtonProps = {
onClick: (event: MouseEvent) => void;
} & StyleProps;
type TextProps = {
fontSize: string;
fontWeight: number;
// ...
} & StyleProps;
Trong trường hợp này, các thành phần UI như Button và Text có những loại Style khác nhau nhưng lại chia sẻ một số thuộc tính giống nhau. Sử dụng Intersection Type cho phép kết hợp các type này một cách linh hoạt mà không cần phải lặp lại những thuộc tính chung nhiều lần. Điều này giúp tạo ra một cách viết code sáng tạo hơn và tiết kiệm thời gian.
Type Composition, hay còn được gọi là Intersection Type, là một chủ đề rất quan trọng và rộng lớn trong TypeScript. Để hiểu rõ hơn về nó, bạn có thể tìm kiếm thêm thông tin trên Internet để tự tìm hiểu và áp dụng trong thực tế.
Conditional Type là gì?
Conditional Type trong TypeScript là một tính năng mạnh mẽ được giới thiệu từ phiên bản 2.8. Đây là một tính năng đặc biệt, cho phép chúng ta tạo ra các loại dữ liệu theo các điều kiện nhất định. Điều này mang lại một hệ thống type cực kỳ linh hoạt và mạnh mẽ cho TypeScript.
Ví dụ cơ bản về Conditional Type là:
T extends U ? X : Y;
Đoạn code này có ý nghĩa rằng nếu type T có thể gán được cho type U, thì kết quả trả về sẽ là type X, ngược lại nếu không thì sẽ trả về type Y. Điều này cho phép chúng ta điều khiển việc gán giá trị cho các type một cách linh hoạt dựa trên các điều kiện logic.
Type Alias là gì?
Type Alias trong TypeScript cho phép bạn tạo ra tên thay thế cho một hoặc nhiều loại dữ liệu, giống như việc tạo Type Alias StringOrNumber cho union type string | number
. Nó cho phép bạn tạo tên gọi mới để đại diện cho một hoặc nhiều loại dữ liệu khác nhau trong mã của bạn. Alias này có thể được sử dụng cho bất kỳ loại dữ liệu nào bạn muốn.
Type Alias và Union Type
Đây là một ví dụ về việc sử dụng Type Alias để áp dụng cho Union Type. Giả sử chúng ta cần tạo một component Flex với một số yêu cầu nhất định:
- Flex có style mặc định là
display: 'flex'
- Flex nhận một Input là flexDirection để áp dụng vào style, ví dụ: flex-direction: flexDirection
type FlexDirection = 'row' | 'column' | 'row-reverse' | 'column-reverse';
@Component({
selector: 'flex-container',
template: `<ng-content></ng-content>`
})
export class FlexComponent {
@Input() flexDirection: FlexDirection = 'row';
@HostBinding('style.display') get display() {
return 'flex';
}
@HostBinding('style.flex-direction') get direction() {
return this.flexDirection;
}
}
Ở đây, chúng ta đã tạo một Type Alias là FlexDirection, đại diện cho các giá trị có thể của flexDirection trong FlexComponent. Bằng cách này, TypeScript sẽ gợi ý cho chúng ta 4 giá trị có thể của flexDirection khi sử dụng component này trong template.
<flex-container>
<button>Submit</button>
<button>Cancel</button>
</flex-container>
<flex-container flexDirection="column">
<input type="email" />
<input type="password" />
</flex-container>
Khi sử dụng component FlexContainer trong template, TypeScript sẽ gợi ý (intellisense) các giá trị của flexDirection
để giúp bạn dễ dàng truyền giá trị hợp lý cho flexDirection
.
Type Alias và Conditional Type
Đây là một ví dụ về việc sử dụng Type Alias kết hợp với Conditional Type cùng với một số built-in types trong TypeScript.
type ObjectDictionary<T> = { [key: string]: T };
type ArrayDictionary<T> = { [key: string]: T[] };
export type Dictionary<T> = T extends []
? ArrayDictionary<T[number]>
: ObjectDictionary<T>;
type StringDictionary = Dictionary<string>; // {[key: string]: string}
type NumberArrayDictionary = Dictionary<number[]>; // {[key: string]: number[]}
type UserEntity = Dictionary<User>; // {[key: string]: User}
Ở ví dụ trên, chúng ta đã định nghĩa ba loại Type Alias: ObjectDictionary
, ArrayDictionary
, và Dictionary
. Trong đó, Dictionary
có thể coi là True Type, còn ObjectDictionary
và ArrayDictionary
được sử dụng để hỗ trợ cho True Type. Điều quan trọng là code này rất dễ hiểu, nói rõ ràng rằng nếu chúng ta truyền vào một type dạng number[]
cho type parameter T ở Dictionary<T>
thì T extends[]
sẽ được đánh giá là true, và Dictionary<number[]>
sẽ trả về type ArrayDictionary<number>
.
Ngoài ra, TypeScript cũng cung cấp một số built-in types rất hữu ích như Exclude, Extract, Readonly, Partial, Nullable, Pick, Record, ReturnType, và Omit:
// Các built-in types
type SomeDiff = Exclude<"a" | "b" | "c", "c" | "d">; // 'a' | 'b'. 'c' đã bị loại bỏ.
type SomeFilter = Extract<"a" | "b" | "c", "c" | "d">; // 'c'. 'a' và 'b' đã bị loại bỏ.
type NonNullable<T> = Exclude<T, null | undefined>; // loại bỏ null và undefined từ T
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };
type Nullable<T> = { [P in keyof T]: T[P] | null };
type Person = {
firstName: string;
lastName: string;
password: string;
};
type PersonWithNames = Pick<Person, "firstName" | "lastName">; // {firstName: string, lastName: string}
type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>; // { prop1: string, prop2: string, prop3: string }
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any; // Trả về type của giá trị mà function T trả về.
type Result = ReturnType<() => string>; // string
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; // loại bỏ type có key là K trong T
type PersonWithoutPassword = Omit<Person, "password">; // {firstName: string, lastName: string}
Các built-in types này cung cấp cho chúng ta rất nhiều khả năng linh hoạt khi tạo và sử dụng các loại type phức tạp trong TypeScript.