In this article, we will be exploring generics in Typescript. Generics allow you to create reusable code that can work with a variety of types, rather than being specific to just one type. This can be especially useful when working with functions and classes that need to operate on a variety of data types.
What are Generics?
Generics are a way to write code that can work with a variety of types, rather than being specific to just one type. They allow you to create code that is more flexible and reusable.
For example, consider a function that takes an array of numbers and returns the maximum value. Without generics, you would have to write a separate function for each data type: one for numbers, one for strings, etc. With generics, you can write a single function that works with any data type:
function findMax<T>(arr: T[]): T {
let max = arr[0];
for (const item of arr) {
if (item > max) {
max = item;
}
}
return max;
}
console.log(findMax([1, 2, 3])); // Outputs: 3
console.log(findMax(['a', 'b', 'c'])); // Outputs: 'c'
In the example above, we have defined a generic function named “findMax” that takes an array of type T and returns a value of type T. The “T” in angle brackets represents the generic type. When we call the function, we specify the type of “T” as a type argument.
Why are Generics Useful?
Generics are useful because they allow you to write code that is flexible and reusable. Instead of having to write separate functions for each data type, you can write a single function that can work with any data type. This can save you a lot of time and make your code easier to maintain.
Generics are also useful because they allow you to write code that is more type-safe. By specifying the type of “T” as a type argument, you can ensure that your function only operates on values of the correct type. This can help prevent type errors and make your code more reliable.
Using Generics with Classes
Generics can also be used with classes in Typescript. For example, consider a simple class that represents a stack:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T {
return this.items.pop();
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // Outputs: 2
const stringStack = new Stack<string>();
stringStack.push('a');
stringStack.push('b');
console.log(stringStack.pop()); // Outputs: 'b'
In the example above, we have defined a class named “Stack” that takes a generic type “T”. The “T” represents the type of the items in the stack. When we create a new instance of the class, we specify the type of “T” as a type argument. This allows us to create stacks of different data types: one for numbers and one for strings.
Conclusion
Generics are a powerful tool in Typescript that allow you to write flexible and reusable code. They can be used with functions and classes to create code that can work with a variety of data types. By specifying the generic type as a type argument, you can ensure that your code is type-safe and operates on the correct data types.
Using generics can save you a lot of time and make your code easier to maintain. They are an important concept to understand when working with Typescript and are useful in a variety of situations.
Exercises
To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.
Write a generic function named “swap” that takes two parameters, “a” and “b”, and swaps their values.
function swap<T>(a: T, b: T): void {
const temp = a;
a = b;
b = temp;
}
let x = 1;
let y = 2;
swap(x, y);
console.log(x, y); // Outputs: 2, 1
let a = 'hello';
let b = 'world';
swap(a, b);
console.log(a, b); // Outputs: 'world', 'hello'
Write a generic function named “reverse” that takes an array and reverses the order of its elements.
function reverse<T>(arr: T[]): T[] {
return arr.reverse();
}
console.log(reverse([1, 2, 3])); // Outputs: [3, 2, 1]
console.log(reverse(['a', 'b', 'c'])); // Outputs: ['c', 'b', 'a']
console.log(reverse([true, false, true])); // Outputs: [true, false, true]
Write a generic function named “removeItem” that takes an array and an item, and removes all occurrences of the item from the array.
function removeItem<T>(arr: T[], item: T): T[] {
return arr.filter(x => x !== item);
}
console.log(removeItem([1, 2, 3, 1, 2, 3], 1)); // Outputs: [2, 3, 2, 3]
console.log(removeItem(['a', 'b', 'c', 'a', 'b', 'c'], 'a')); // Outputs: ['b', 'c', 'b', 'c']
console.log(removeItem([true, false, true, false], true)); // Outputs: [false, false]
Write a generic function named “groupBy” that takes an array and a key selector function, and returns an object where the keys are the selected values and the values are arrays of the corresponding elements.
function groupBy<T, K>(arr: T[], keySelector: (item: T) => K): Record<K, T[]> {
const groups: Record<K, T[]> = {};
for (const item of arr) {
const key = keySelector(item);
if (key in groups) {
groups[key].push(item);
} else {
groups[key] = [item];
}
}
return groups;
}
console.log(groupBy([1, 2, 3, 4, 5], x => x % 2 === 0));
// Outputs: { 'false': [1, 3, 5], 'true': [2, 4] }
Write a generic function named “unique” that takes an array and returns a new array with duplicate elements removed.
function unique<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
}
console.log(unique([1, 2, 3, 2, 1])); // Outputs: [1, 2, 3]
console.log(unique(['a', 'b', 'c', 'b', 'a'])); // Outputs: ['a', 'b', 'c']
console.log(unique([true, false, true, false])); // Outputs: [true, false]