Understanding TypeScript Generics
So, what are Generics in TypeScript?
TypeScript Generics is a tool that helps you write flexible code. It can handle many different data types while still keeping your code safe from errors.
Think of it as building blocks that you can use over and over again to create different things, like functions or data structures, without worrying about making mistakes with the type of data.
Now, these ‘generics‘ use something called ‘type parameters’. You can think of these as empty boxes that can hold any type of data.
We write these inside angle brackets (<>).
You can use these ‘type parameters’ anywhere in your code to tell TypeScript what type of data you’re working with, such as the type of a variable, the input of a function, or what a function returns.
Let’s check out a basic example so that you can understand what I am talking about:
function example<T>(arg: T): T { ✅
return arg;
}
let output = example<string>("Hi, Welcome to Itsourcecode!");
console.log(output);
In our given example, the “example” is a generic function that accepts a type parameter, denoted as ‘T’.
The argument ‘arg’ is of the same type ‘T’, and the function’s return type is also ‘T’.
When invoking identity(“Hi, Welcome to Itsourcecode!“), the type parameter ‘T’ is automatically determined to be a string, which guarantees type safety.
Output:
Hi, Welcome to Itsourcecode!
Let’s check out another example if you’re still confused:
// ⬇ define a generic value called T with <T>
function getFirstElement<T>(arr: T[]): T { ✅
return arr[0];
}
type Grades = number;
type Subjects = string;
const numberArray: Grades[] = [80, 85, 88, 90, 95];
const stringArray: Subjects[] = ['Programming', 'Software Development', 'Web Development'];
// ⬇ Note the generic values being passed in <Grades> & <Subjects>
const firstGrades = getFirstElement<Grades>(numberArray);
const firstSubjects = getFirstElement<Subjects>(stringArray);
console.log(firstGrades);
console.log(firstSubjects);
Let’s break down the example above:
- Generic Function
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
This is a generic function named getFirstElement. The <T> is a placeholder for any type.
This function takes an array arr of any type T and returns the first element of the array, which is also of type T.
- Type Aliases
type Grades = number;
type Subjects = string;
As you can see, Grades and Subjects are type aliases. Grades is an alias for the number type and Subjects is an alias for the string type.
- Arrays
const numberArray: Grades[] = [80, 85, 88, 90, 95];
const stringArray: Subjects[] = ['Programming', 'Software Development', 'Web Development'];
‘numberArray’ is an array of “Grades” (which is an alias for ‘number’), and ‘stringArray’ is an array of “Subjects” (which is an alias for ‘string’).
Output:
80
Programming
- Using the Generic Function
const firstGrades = getFirstElement<Grades>(numberArray);
const firstSubjects = getFirstElement<Subjects>(stringArray);
As you can see, the getFirstElement function is called with <Grades> and <Subjects> as the generic types.
This means the function will return the first element of the numberArray and stringArray, respectively.
And lastly, we have the output:
- Output
console.log(firstGrades);
console.log(firstSubjects);
These lines will print the first grade and the first subject to the console.
80
Programming
How to use Generics in TypeScript?
Generics in TypeScript are used to help you make the parts of your code (like functions, classes, or interfaces) reusable and flexible.
It’s like using a placeholder for different types of data, which you can fill in later when you use that part of the code.
This way, you can use the same piece of code with different types of data, and TypeScript will still keep your code safe from type-related errors.
Example of Generics functions
Generic functions enable you to establish a function with a placeholder for a type, which can be defined when the function is invoked.
function example<T>(arg: T): T {
return arg;
}
// Usage
let result1 = example<string>("Itsourcecode");
let result2 = example<number>(18);
console.log(result1);
console.log(result2);
As you can see, T is a type variable that can be any type. When you call ‘example’, you specify the type you want to use.
Output:
Itsourcecode
18
Example of Generics with interfaces
Generics can be combined with interfaces to establish custom types for the properties of the object that the interface represents.
Here’s a simple illustration of how generics can be utilized with an interface.
// Define a generic interface
interface GenericInterface<T> {
(arg: T): T;
}
// Implement the generic interface with a number
let numberExample: GenericInterface<number> = (arg) => {
return arg;
}
console.log(numberExample(143));
// Implement the generic interface with a string
let stringExample: GenericInterface<string> = (arg) => {
return arg;
}
console.log(stringExample("Hi, Welcome to Itsourcecode!"));
In this example, GenericInterface is a generic interface that takes a type T. It defines a function that takes an argument arg of type T and returns a value of type T.
Then, we create two examples: numberExample and stringExample. numberExample is a function that takes a number and returns a number. stringExample is a function that takes a string and returns a string.
Output:
143
Hi, Welcome to Itsourcecode!
Example of Generics with types
The application of generics with types bears a strong resemblance to their use with interfaces, adhering to the same guidelines we previously discussed.
However, in this case, we employ the syntax specific to types.
// Define a generic type
type GenericType<T> = T;
// Use the generic type with number
let numberExample: GenericType<number> = 111;
console.log(numberExample);
// Use the generic type with string
let stringExample: GenericType<string> = "Hi, Welcome to Itsourcecode!";
console.log(stringExample);
In our given example, GenericType is a generic type that takes a type T.
Then, we create two examples: numberExample and stringExample. numberExample is a variable of type GenericType, and stringExample is a variable of type GenericType.
Output:
111
Hi, Welcome to Itsourcecode!
Example of Generics with classes
Generics can also be applied to classes, enabling the creation of class definitions that are adaptable and can be reused.
// Define a generic class
class GenericClass<T> {
value: T;
add: (x: T, y: T) => T;
}
// Implement the generic class with number
let numberExample = new GenericClass<number>();
numberExample.value = 20;
numberExample.add = function(x, y) { return x + y; };
console.log(numberExample.add(numberExample.value, 50));
// Implement the generic class with string
let stringExample = new GenericClass<string>();
stringExample.value = "Hi, ";
stringExample.add = function(x, y) { return x + y; };
console.log(stringExample.add(stringExample.value, "Welcome to Itsourcecode!"));
As you can see in our given example, GenericClass is a generic class that takes a type T.
It has a property value of type T and a method add that takes two parameters of type T and returns a value of type T.
Then, we create two examples: numberExample and stringExample. numberExample is an instance of GenericClass with number as the type parameter, and stringExample is an instance of GenericClass with string as the type parameter.
Output:
70
Hi, Welcome to Itsourcecode!
Example of Multiple Type Variables
Functions, interfaces, or classes can be defined with several type variables.
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// Usage
const mergedObj = merge({ name: "Itsourcecode" }, { age: 18 });
console.log(mergedObj.name);
console.log(mergedObj.age);
Output:
Itsourcecode
18
How to use Constraints on Generics?
At times, you may wish to restrict the types that can be passed as arguments for a generic type.
This is the role of constraints.
// Define a generic function with a constraint
function printLength<T extends { length: number }>(arg: T): T {
console.log(arg.length); // Now we know .length is a property on arg
return arg;
}
// Use the function with an array (arrays have a length property)
let array = printLength([10, 20, 30, 40, 50]);
console.log(array); // Outputs: [10, 20, 30, 40, 50]
// Use the function with a string (strings have a length property)
let string = printLength("Welcome to Itsourcecode!");
console.log(string); // Outputs: Welcome to Itsourcecode!
As you can see in our example, printLength is a generic function that takes a type T that extends { length: number }.
This means T must be a type that has a length property of type number. The function prints the length of arg and returns arg.
Then, we call printLength with an array and a string. Both arrays and strings have a length property, so they satisfy the constraint { length: number }.
Output:
5
[ 10, 20, 30, 40, 50 ]
24
Welcome to Itsourcecode!
Conclusion
So, that’s the end of our discussion about Generics in TypeScript, which is a powerful tool for creating reusable, flexible, and type-safe code.
By using generics, you can create functions, classes, and interfaces that work with any data type, while maintaining type safety.
Experiment with the provided examples to gain a better understanding of how generics can be utilized in your TypeScript projects.
I hope this article has helped you understand TypeScript Generics.
If you have any questions or inquiries, please don’t hesitate to leave a comment below.