Tìm hiểu về Generics trong TypeScript
Trong bài viết này là mình giúp bạn hiểu rõ về khái niệm Generics trong TypeScript và cách sử dụng chúng trong việc tạo mã nguồn linh hoạt và tái sử dụng. Mình sẽ tìm hiểu về cú pháp cơ bản của Generics, cách áp dụng chúng cho hàm, lớp, cũng như sử dụng trong các interface và class. Bài viết cũng sẽ đề cập đến việc giới hạn kiểu dữ liệu bằng Generics và cách rút trích kiểu dữ liệu tự động.
Cuối cùng, mình sẽ xem xét lợi ích và hạn chế của việc sử dụng Generics, cùng với các ví dụ minh họa thực tế để củng cố kiến thức. Hãy cùng tìm hiểu ngay bài viết cùng mình nhé!
Generics là gì?
Generics là một tính năng trong TypeScript (và cũng xuất hiện trong nhiều ngôn ngữ lập trình khác) cho phép bạn làm cho kiểu dữ liệu của một biến, hàm, lớp hoặc interface trở nên động, tức là kiểu dữ liệu có thể được thay đổi tùy theo cách mà bạn sử dụng nó. Generics giúp bạn viết mã linh hoạt và tái sử dụng hơn bằng cách cho phép bạn xác định kiểu dữ liệu một cách động, chẳng hạn kiểu dữ liệu của một biến hoặc đối số của một hàm.
Trong TypeScript, bạn có thể sử dụng Generics bằng cách sử dụng các tham số kiểu (type parameters) khi định nghĩa hàm, lớp, interface, hoặc kiểu dữ liệu. Tham số kiểu này sẽ là kiểu dữ liệu tùy chỉnh, cho phép bạn truyền kiểu dữ liệu cụ thể khi bạn sử dụng thực thể (instance) của hàm, lớp, hoặc interface đó.
Bài viết này được đăng tại [free tuts .net]
Ví dụ đơn giản về Generics trong TypeScript:
function identity<T>(arg: T): T { return arg; } let output = identity<string>("Hello, Generics!");
Trong ví dụ trên, hàm identity sử dụng tham số kiểu T, cho phép bạn truyền vào nó một kiểu dữ liệu cụ thể khi gọi hàm. Generics giúp hàm identity trả về cùng kiểu dữ liệu mà bạn truyền vào.
Lợi ích của Generics
Generics cung cấp nhiều lợi ích quan trọng cho TypeScript:
-
Tái sử dụng mã nguồn: Generics giúp bạn viết mã nguồn một lần và tái sử dụng nó với nhiều kiểu dữ liệu khác nhau. Điều này giúp giảm mã trùng lặp.
-
Kiểm tra kiểu tại thời gian biên dịch: TypeScript kiểm tra kiểu dữ liệu tại thời gian biên dịch, giúp tránh lỗi thời gian chạy do kiểu dữ liệu không phù hợp.
-
Tích hợp với các thư viện bên ngoài: Generics cho phép bạn tương tác với các thư viện bên ngoài và xác định kiểu dữ liệu một cách dễ dàng, giúp giảm nguy cơ lỗi.
Hạn chế và khi nào nên tránh sử dụng
Mặc dù Generics là một công cụ mạnh, nhưng cũng có những hạn chế và trường hợp nên tránh sử dụng:
-
Phức tạp quá mức: Sử dụng Generics không cần thiết có thể làm cho mã nguồn trở nên phức tạp và khó hiểu.
-
Overusing Generics: Việc sử dụng Generics cho mọi thứ có thể gây hiệu suất yếu và làm cho mã nguồn trở nên khó quản lý. Hãy sử dụng Generics khi thực sự cần thiết.
-
Sự hiểu biết về kiểu dữ liệu: Generics yêu cầu bạn hiểu biết về kiểu dữ liệu và cách chúng hoạt động. Nếu không hiểu rõ, có thể dẫn đến lỗi logic.
-
Sự phụ thuộc vào API bên ngoài: Sử dụng Generics để tạo sự phụ thuộc vào API bên ngoài có thể tạo ra các ràng buộc không cần thiết và làm cho mã nguồn dễ bị thay đổi.
Cú pháp và sử dụng cơ bản
Sử dụng Generics cho hàm và lớp
Generics có thể được sử dụng cho hàm và lớp trong TypeScript. Dưới đây là cú pháp cơ bản cho cả hai trường hợp:
Generics cho hàm:
function doSomething<T>(arg: T): T { // Các xử lý với biến arg return arg; } let result = doSomething<string>("Hello, Generics!");
Trong ví dụ trên, hàm doSomething
nhận một tham số kiểu T và trả về cùng kiểu dữ liệu.
Generics cho lớp:
class GenericBox<T> { private value: T; constructor(initialValue: T) { this.value = initialValue; } getValue(): T { return this.value; } } let numberBox = new GenericBox<number>(42); let stringBox = new GenericBox<string>("Hello, Generics!"); console.log(numberBox.getValue()); // 42 console.log(stringBox.getValue()); // "Hello, Generics!"
Trong ví dụ về lớp GenericBox
, kiểu T là kiểu dữ liệu của value, và bạn có thể tạo các thực thể của lớp với các kiểu dữ liệu khác nhau.
Ví dụ minh họa
Một ví dụ cụ thể về việc sử dụng Generics cho hàm:
function firstElement<T>(arr: T[]): T | undefined { if (arr.length === 0) { return undefined; } return arr[0]; } const numbers = [1, 2, 3, 4, 5]; const firstNum = firstElement(numbers); // firstNum có kiểu number const names = ["Alice", "Bob", "Charlie"]; const firstName = firstElement(names); // firstName có kiểu string
Hàm firstElement
sử dụng Generics để trả về phần tử đầu tiên của một mảng bất kỳ, bất kể kiểu dữ liệu của mảng đó.
Parameter Types và Return Types
Sử dụng Generics cho Parameter Types
Generics cho parameter types
cho phép bạn chỉ định kiểu dữ liệu của tham số đầu vào cho hàm, và kiểu dữ liệu này có thể thay đổi động dựa trên cách bạn gọi hàm.
function combine<T>(input1: T, input2: T): T { return input1 + input2; } const result = combine<number>(2, 3); // Kết quả có kiểu number
Trong ví dụ trên, hàm combine
nhận hai tham số kiểu T, và kiểu của kết quả sẽ là cùng kiểu T. Bằng cách sử dụng Generics, bạn có thể chỉ định kiểu dữ liệu của result khi gọi hàm.
Sử dụng Generics cho Return Types
Bạn cũng có thể sử dụng Generics để định nghĩa kiểu trả về của một hàm.
function createArray<T>(value: T, times: number): T[] { const result: T[] = []; for (let i = 0; i < times; i++) { result.push(value); } return result; } const newArray = createArray<string>("Hello", 3); // newArray có kiểu string[]
Trong ví dụ này, hàm createArray
trả về một mảng kiểu T, và bạn có thể chỉ định kiểu trả về khi gọi hàm.
Ví dụ minh họa
Một ví dụ cụ thể về việc sử dụng Generics cho parameter types
và return types:
function mergeArrays<T>(arr1: T[], arr2: T[]): T[] { return arr1.concat(arr2); } const numArray1 = [1, 2, 3]; const numArray2 = [4, 5, 6]; const mergedNumArray = mergeArrays<number>(numArray1, numArray2); // mergedNumArray có kiểu number[]
Giới hạn (Constraints) trong Generics
Giới hạn trong Generics cho phép bạn xác định rõ hơn kiểu dữ liệu cho tham số generics bằng cách áp dụng một số ràng buộc.
Sử dụng giới hạn để hạn chế kiểu dữ liệu
Giới hạn trong Generics có thể được sử dụng để hạn chế kiểu dữ liệu cho tham số. Ví dụ, nếu bạn muốn chỉ cho phép sử dụng Generics với kiểu dữ liệu là các lớp có một phương thức cụ thể, bạn có thể sử dụng giới hạn như sau:
interface Shape { calculateArea(): number; } function calculateTotalArea<T extends Shape>(shapes: T[]): number { let totalArea = 0; shapes.forEach(shape => { totalArea += shape.calculateArea(); }); return totalArea; } class Circle implements Shape { constructor(public radius: number) {} calculateArea() { return Math.PI * this.radius * this.radius; } } class Rectangle implements Shape { constructor(public width: number, public height: number) {} calculateArea() { return this.width * this.height; } } const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)]; const totalArea = calculateTotalArea(shapes); // Đúng
Trong ví dụ này,mình sử dụng giới hạn <T extends Shape>
để đảm bảo rằng T phải là một lớp thỏa mãn giao diện Shape.
Ví dụ minh họa
Một ví dụ cụ thể về việc sử dụng giới hạn trong Generics:
interface Lengthwise { length: number; } function getArrayLength<T extends Lengthwise>(arg: T): number { return arg.length; } const result1 = getArrayLength("Hello"); // result1 có kiểu number const result2 = getArrayLength([1, 2, 3, 4, 5]); // result2 có kiểu number
Trong ví dụ này, mình sử dụng giới hạn để đảm bảo rằng tham số arg
phải có thuộc tính length
kiểu number
.
Generics trong Interface và Class
Sử dụng Generics trong Interface
Generics có thể được sử dụng trong interface để xây dựng các giao diện có thể làm việc với nhiều kiểu dữ liệu khác nhau. Ví dụ:
interface Pair<T, U> { first: T; second: U; } const pair1: Pair<number, string> = { first: 1, second: "two" }; const pair2: Pair<string, boolean> = { first: "hello", second: true };
Trong ví dụ này, mình đã xây dựng một giao diện Pair sử dụng Generics để cho phép định nghĩa kiểu dữ liệu cho first
và second
.
Sử dụng Generics trong Class
Generics cũng có thể được sử dụng trong class để tạo các lớp có thể làm việc với nhiều kiểu dữ liệu khác nhau. Ví dụ:
class Box<T> { private content: T; constructor(value: T) { this.content = value; } getContent(): T { return this.content; } } const numberBox = new Box<number>(42); const stringBox = new Box<string>("Hello, Generics!"); const numberContent = numberBox.getContent(); // numberContent có kiểu number const stringContent = stringBox.getContent(); // stringContent có kiểu string
Trong ví dụ này, mình đã tạo một class Box
sử dụng Generics để cho phép định nghĩa kiểu dữ liệu cho nội dung trong hộp.
Ví dụ minh họa
Một ví dụ minh họa về việc sử dụng Generics trong interface và class:
interface KeyValuePair<K, V> { key: K; value: V; } class Dictionary<K, V> { private items: KeyValuePair<K, V>[] = []; add(key: K, value: V) { this.items.push({ key, value }); } getValue(key: K): V | undefined { const pair = this.items.find(item => item.key === key); return pair ? pair.value : undefined; } } const numberDict = new Dictionary<number, string>(); numberDict.add(1, "one"); numberDict.add(2, "two"); const wordDict = new Dictionary<string, number>(); wordDict.add("three", 3); wordDict.add("four", 4); const numberValue = numberDict.getValue(1); // numberValue có kiểu string const wordValue = wordDict.getValue("three"); // wordValue có kiểu number
Trong ví dụ này,mình đã sử dụng Generics trong interface KeyValuePair
và class Dictionary để tạo một từ điển cho các kiểu dữ liệu khác nhau.
Inference (Rút trích) kiểu dữ liệu
Rút trích kiểu dữ liệu tự động
Một trong những tính năng mạnh mẽ của TypeScript là khả năng tự động rút trích kiểu dữ liệu. Điều này có nghĩa rằng TypeScript có thể suy luận và gán kiểu dữ liệu cho biến dựa trên giá trị mà bạn gán cho nó. Ví dụ:
const numberValue = 42; // TypeScript tự động rút trích kiểu number cho numberValue const stringValue = "Hello, Inference!"; // TypeScript tự động rút trích kiểu string cho stringValue
Sử dụng inference để làm cho mã nguồn ngắn gọn hơn
Khi bạn sử dụng Generics, inference giúp bạn viết mã nguồn ngắn gọn hơn bằng cách tự động xác định kiểu dữ liệu dựa trên tham số bạn truyền vào hàm hoặc lớp Generic. Ví dụ:
function identity<T>(arg: T): T { return arg; } const num = identity(42); // TypeScript tự động rút trích kiểu number cho num const str = identity("TypeScript"); // TypeScript tự động rút trích kiểu string cho str
Ví dụ minh họa
Một ví dụ minh họa về việc sử dụng inference:
function multiply(a: number, b: number) { return a * b; } const result = multiply(5, 6); // TypeScript tự động rút trích kiểu number cho result
Trong ví dụ này, TypeScript tự động rút trích kiểu number cho biến result
dựa trên kết quả của hàm multiply
.
Ví dụ minh họa
Sử dụng Generics để tái sử dụng logic
Ví dụ về sử dụng Generics để tái sử dụng logic trong hàm đảo ngược mảng:
function reverse<T>(arr: T[]): T[] { return arr.reverse(); } const numbers = [1, 2, 3]; const reversedNumbers = reverse(numbers); // reversedNumbers có kiểu number[]
Sử dụng Generics trong các tình huống phức tạp
Generics có thể được sử dụng trong nhiều tình huống phức tạp như xây dựng thư viện, quản lý trạng thái ứng dụng, và xử lý dữ liệu động. Việc này giúp mã nguồn dễ dàng mở rộng và duyệt qua các kiểu dữ liệu khác nhau mà không cần viết mã nguồn gần như giống nhau.
Kết quả
Trong bài viết này, mình đã tìm hiểu về Generics trong TypeScript, một tính năng mạnh mẽ cho phép ta làm việc với các kiểu dữ liệu động và tạo mã nguồn dễ tái sử dụng. Generics cho phép ta xác định kiểu dữ liệu linh hoạt cho biến, hàm, lớp và giao diện. Mình đã thấy cách sử dụng Generics để giải quyết nhiều tình huống thực tế và tận dụng lợi ích của việc kiểm tra kiểu tại thời gian biên dịch.
Tuy Generics là một công cụ mạnh mẽ, nhưng cũng cần hiểu cách sử dụng chúng một cách hợp lý. Nếu không sử dụng đúng cách, Generics có thể làm cho mã nguồn trở nên phức tạp và khó quản lý. Hãy sử dụng Generics khi thực sự cần thiết và khi muốn tạo sự linh hoạt và tái sử dụng trong mã nguồn TypeScript của bạn.
Mình đã thấy Generics có thể được sử dụng trong nhiều tình huống, từ xử lý dữ liệu động đến quản lý trạng thái ứng dụng và xây dựng thư viện. Nhờ vào tính linh hoạt của Generics, mình có thể mở rộng ứng dụng và duyệt qua các kiểu dữ liệu khác nhau mà không cần viết mã nguồn gần như giống nhau.
Hy vọng rằng bài viết này đã giúp bạn hiểu rõ hơn về Generics trong TypeScript và cách áp dụng chúng vào công việc lập trình hàng ngày.