Các Kỹ Thuật Nâng Cao Trong TypeScript
Mình rất vui được chia sẻ những kỹ thuật nâng cao trong TypeScript mà có thể bạn chưa biết đến. Với vai trò là một lập trình viên chuyên nghiệp, mình sẽ giải thích các khái niệm này thông qua các ví dụ thực tế và cụ thể. Đây là những mẹo và thủ thuật giúp bạn khai thác tối đa sức mạnh của TypeScript trong các dự án của mình. Hãy cùng bắt đầu nhé!
1. Kết hợp Chuỗi Ký Tự với Template Literals
Template literals trong TypeScript không chỉ dùng để tạo ra các chuỗi động mà còn có thể sử dụng trong việc tạo ra các kiểu chuỗi mới. Bằng cách kết hợp với các kiểu dữ liệu khác, bạn có thể tạo ra các kiểu dữ liệu mới một cách linh hoạt.
type EventName<T extends string> = `${T}Updated`;
type OrderEvent = EventName<"order">; // Kết quả: "orderUpdated"
Điều này thực sự hữu ích khi bạn cần xây dựng một hệ thống sự kiện hoặc muốn duy trì một quy tắc đặt tên nhất quán trong toàn bộ codebase.
2. Kiểu 'Branded' với Intersection Types
Khi bạn cần tạo ra các kiểu dữ liệu độc nhất mà không thể nhầm lẫn với các kiểu khác dù có cùng cấu trúc, bạn có thể sử dụng 'Branded' types. Đây là cách tuyệt vời để tránh việc trộn lẫn các kiểu dữ liệu mà bạn không mong muốn.
type CustomerId = string & { readonly brand: unique symbol };
type ProductId = string & { readonly brand: unique symbol }; function createCustomerId(id: string): CustomerId { return id as CustomerId;
} function createProductId(id: string): ProductId { return id as ProductId;
} const customerId = createCustomerId("cust123");
const productId = createProductId("prod456"); // Dòng code dưới đây sẽ gây lỗi kiểu dữ liệu
// const error = customerId = productId;
Với phương pháp này, dù cả CustomerId
và ProductId
đều là chuỗi dưới nền, chúng không thể thay thế cho nhau, giúp bảo vệ mã nguồn của bạn khỏi các lỗi không mong muốn.
3. Sử Dụng Từ Khóa infer
trong Conditional Types
infer
là một từ khóa mạnh mẽ trong TypeScript, cho phép bạn trích xuất thông tin kiểu từ các kiểu phức tạp. Điều này đặc biệt hữu ích khi bạn cần làm việc với các hàm hoặc promise và muốn tận dụng khả năng suy luận kiểu của TypeScript.
type ExtractPromiseType<T> = T extends Promise<infer U> ? U : T; type ResultType = ExtractPromiseType<Promise<number>>; // Kết quả: number
type NonPromiseType = ExtractPromiseType<string>; // Kết quả: string // Một ví dụ khác: Trích xuất kiểu trả về của hàm
type ReturnTypeOfFunction<T> = T extends (...args: any[]) => infer R ? R : never; function getUser() { return { id: 1, name: "Alice" }; }
type UserType = ReturnTypeOfFunction<typeof getUser>; // Kết quả: { id: number; name: string; }
Sự linh hoạt mà infer
mang lại giúp bạn tạo ra các kiểu dữ liệu có khả năng tái sử dụng cao và dễ duy trì.
4. Sử dụng Template Literal Types để Kết Hợp Kiểu Chuỗi
Template literal types cho phép bạn tạo ra các kiểu dữ liệu dựa trên sự kết hợp của nhiều literal types khác nhau. Đây là một tính năng rất mạnh mẽ, đặc biệt khi bạn cần áp dụng các ràng buộc kiểu chuỗi trong dự án.
type ThemeVariant = "light" | "dark";
type Color = "red" | "green" | "blue";
type Theme = `${ThemeVariant}-${Color}`; // Theme giờ đây có thể là một trong các giá trị:
// "light-red" | "light-green" | "light-blue" | "dark-red" | "dark-green" | "dark-blue" function applyTheme(theme: Theme) { // Code áp dụng theme tại đây
} applyTheme("dark-blue"); // OK
// applyTheme("bright-yellow"); // Lỗi: "bright-yellow" không khớp với kiểu Theme
Tính năng này rất hữu ích khi bạn cần quản lý các biến thể giao diện người dùng hoặc định nghĩa các route API với các pattern cụ thể.
5. Đệ Quy Kiểu Dữ Liệu
Đệ quy kiểu dữ liệu trong TypeScript cho phép bạn định nghĩa các kiểu có thể tự tham chiếu chính nó. Điều này đặc biệt cần thiết khi làm việc với các cấu trúc dữ liệu lồng nhau hoặc dạng cây (tree).
type JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; const configData: JSONValue = { appName: "MyApp", version: 1.0, settings: { theme: "dark", languages: ["en", "fr"], isBeta: true }
};
Khi bạn phải làm việc với các cấu trúc JSON phức tạp, kiểu JSONValue
này sẽ giúp bạn duy trì tính chính xác và nhất quán trong việc định nghĩa kiểu.
6. Variadic Tuple Types cho Tuples Linh Hoạt
TypeScript 4.0 đã giới thiệu kiểu tuple biến đổi (variadic tuple types), cho phép bạn xử lý các tuple một cách linh hoạt hơn. Đây là công cụ hữu ích khi làm việc với các hàm có số lượng tham số biến đổi hoặc khi cần kết hợp các tuple động.
type MergeTuples<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Combined = MergeTuples<[1, 2], [3, 4]>; // Kết quả: [1, 2, 3, 4] function combineTuples<T extends unknown[], U extends unknown[]>(tuple1: T, tuple2: U): MergeTuples<T, U> { return [...tuple1, ...tuple2];
} const result = combineTuples([1, 2], [3, 4]); // Kết quả: [1, 2, 3, 4]
Kỹ thuật này giúp bạn thực hiện các thao tác an toàn trên các tuple, rất hữu ích khi làm việc với các API trả về hoặc yêu cầu cấu trúc tuple cụ thể.
7. Chuyển Đổi Khóa với as
trong Mapped Types
Khi làm việc với mapped types, as
cho phép bạn chuyển đổi khóa của một đối tượng theo ý muốn. Đây là kỹ thuật tuyệt vời để tạo ra các kiểu dữ liệu phái sinh với tên thuộc tính đã được chỉnh sửa.
type MethodNames<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}; interface UserProfile { name: string; age: number;
} type UserMethods = MethodNames<UserProfile>;
// Kết quả là một đối tượng với các phương thức:
// {
// getName: () => string;
// getAge: () => number;
// }
8. const
Assertions để Tạo Literal Types Cụ Thể
Sử dụng const
assertions có thể giúp bạn tạo ra các kiểu literal cụ thể từ các giá trị tại thời điểm runtime. Điều này rất có lợi khi bạn muốn dùng các giá trị runtime như là các kiểu dữ liệu.
const statusCodes = ["OK", "ERROR", "LOADING"] as const;
type StatusCode = typeof statusCodes[number]; // Kết quả: "OK" | "ERROR" | "LOADING" function handleStatus(status: StatusCode) { // Xử lý trạng thái
} handleStatus("OK"); // OK
// handleStatus("FAIL"); // Lỗi: "FAIL" không phải là một StatusCode hợp lệ
Với const
assertions, bạn có thể duy trì sự nhất quán giữa các giá trị thực và kiểu dữ liệu, giảm thiểu rủi ro xảy ra lỗi không mong muốn.
9. Sử Dụng Discriminated Unions với never
để Kiểm Tra Mẫu Toàn Diện
Discriminated unions là cách tuyệt vời để mô hình hóa các trạng thái loại trừ lẫn nhau trong TypeScript. Khi kết hợp với kiểu never
, bạn có thể đạt được khả năng kiểm tra mẫu toàn diện, giúp đảm bảo tất cả các trường hợp đều được xử lý.
type Shape = | { kind: "circle"; radius: number } | { kind: "square"; sideLength: number } | { kind: "triangle"; base: number; height: number }; function calculateArea(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.sideLength ** 2; case "triangle": return 0.5 * shape.base * shape.height; default: const exhaustiveCheck: never = shape; return exhaustiveCheck; }
}
Kết luận
Trên đây là các kỹ thuật nâng cao trong TypeScript có thể giúp bạn tối ưu hóa và bảo vệ mã nguồn của mình. Sử dụng chúng một cách hợp lý sẽ giúp bạn đạt được hiệu quả cao hơn trong việc phát triển ứng dụng, cũng như tăng cường sự an toàn và khả năng tái sử dụng mã. Hy vọng những chia sẻ này sẽ giúp ích cho bạn trong công việc hàng ngày!
Cảm ơn anh em đã đọc bài viết của mình. Hi vọng bài viết sẽ giúp ích cho anh em.
Anh em hãy theo giõi mình để có thêm nhiều bài viết hay và bổ ích nhé !
Xem bài viết gốc tại đây