Following the Don’t-Repeat-Yourself (DRY) principle is important when it comes to writing dynamic and reusable code. Using generics can help you achieve this in your TypeScript code.
With generics, you can write dynamic and reusable generic blocks of code. Furthermore, you can apply generics in TypeScript to classes, interfaces, and functions.
In this article, you will integrate generics into your TypeScript code and apply them to functions and classes. You will also learn how to add constraints to generics in TypeScript by using interfaces.
To successfully complete this tutorial, you will need the following:
ts-node
installed. This is necessary if you would like to test and run your TypeScript code. This Running TypeScript Scripts With Ease with ts-node tutorial is a great place to start.Sometimes, you may want to repeat the same block of code for different data types. Here’s an example of the same function being used for two different data types:
// for number type
function fun(args: number): number {
return args;
}
// for string type
function fun(args: string): string {
return args;
}
Note that, in this example, the same function is being repeated for number
and string
types. Generics can help you to write generalized methods instead of writing and repeating the same block of code, as in the above example.
There is a type called any
, which you can use to achieve the same effect as generics in your code. Using the any
type will allow you to opt-out of type-checking. However, any
is not type-safe
. This means that using any
can give you an exception.
To see this in practice, apply the any
type to the previous code example:
function fun(args: any): any {
return args;
}
Swapping number
and string
types to the any
type makes the function generic. But there’s a catch—using the any
type means that the fun
function can accept any data. As a result, you are losing type safety as well.
Although using the any
type is a way to make your TypeScript code more generic, it may not always be the best option. In the next step, you explore a different option for creating type-safe generics.
To create type-safe generics, you will need to use Type
parameters. Type
parameters are defined by T
or <T>
. They denote the data type of passed parameters to a class, interface, and functions.
Returning to the fun
function, use T
to make your generic function type-safe:
function fun<T>(args:T):T {
return args;
}
As a result, fun
is now a type-safe generic function. To test this type-safe generic function, create a variable named result
and set it equal to fun
with a string
type parameter. The argument will be the Hello World
string:
let result = fun<string>("Hello World");
Try using the fun
function with the number
type. Set the argument equal to 200
:
let result2 = fun<number>(200);
If you would like to see the results of this code, you can include console.log
statements to print result
and result2
to the console:
console.log(result);
console.log(result2);
In the end, your code should look like this:
function fun<T>(args:T):T {
return args;
}
let result = fun<string>("Hello World");
let result2 = fun<number>(200);
console.log(result);
console.log(result2);
Use ts-node
to run this TypeScript code in the console:
- npx ts-node index.ts
The code renders without error. You will see this output:
OutputHello World
200
You can now create type-safe generics for functions with one parameter. It’s also important to know how to create generics for functions with multiple parameters of many different types.
If there are many parameters in a function, you can use different letters to denote the types. You don’t have to only use T
:
function fun<T, U, V>(args1:T, args2: U, args3: V): V {
return args3;
}
This function takes 3 parameters, args1
, args2
, and arg3
, and returns args3
. These parameters are not restricted to a certain type. This is because T
, U
, and V
are used as generic types for the fun
function’s parameters.
Create a variable called result3
and assign it to fun
. Include the <string, number, boolean>
types to fill in the T
, U
, and V
generic types. For the arguments, include a string, a number, and boolean held within parentheses:
let result3 = fun<string, number, boolean>('hey', 3, false);
This will return the third argument, false
. To see this, you can use a console.log
statement:
console.log(result3);
Run the ts-node
command to see your console.log
statement output:
- npx ts-node params.ts
This will be the output:
Outputfalse
Now you can create generic types for functions with multiple parameters. Like functions, generics can be used with classes
and interfaces
as well.
Like generic functions, classes can be generic too. The type
parameter in angle (<>
) brackets are used, as with functions. Then the <T>
type is used throughout the class for defining methods and properties.
Create a class that takes both number
and string
inputs and creates an array with those inputs. Use <T>
as the generic type parameter:
class customArray<T> {
private arr: T[] = [];
}
Now, your array that takes in items of different types is in place. Create a method called getItems
that returns the customArray
array:
getItems (arr: T[]) {
return this.arr = arr;
}
Create a method called addItem
that adds new items to the end of the customArray
array:
addItem(item:T) {
this.arr.push(item);
}
The arr: T[]
argument means that the items within the array can be of any type. So customArray
can be an array of numbers, booleans, or strings.
Add a method called removeItem
that removes specified items from the customArray
:
removeItem(item: T) {
let index = this.arr.indexOf(item);
if(index > -1)
this.arr.splice(index, 1);
}
Like the addItem
method, removeItem
takes a parameter of any type and removes the specified parameter from the customArray
array.
Now the generic class customArray
is complete. Create an instance of customArray
for number
and string
types.
Declare a variable called numObj
set equal to an instance of customArray
for number
types:
let numObj = new customArray<number>();
Use the addItem
method to add the number 10
to numObj
:
numObj.addItem(10);
Since customArray
is generic, it can also be used to create an array of strings. Create a variable called strObj
set equal to an instance of customArray
for string types:
let strObj = new customArray<string>();
Use the addItem
method to add the string Robin
to the strObj
array.
strObj.addItem(“Robin”);
To see the results of your code, create a console.log
statement for both numObj
and strObj
:
console.log(numObj);
console.log(strObj);
In the end, your code should look like this:
class customArray<T> {
private arr: T[] = [];
getItems(arr: T[]) {
return this.arr = arr;
}
addItem(item:T) {
this.arr.push(item);
}
removeItem(item: T) {
let index = this.arr.indexOf(item);
if(index > -1)
this.arr.splice(index, 1);
}
}
let numObj = new customArray<number>();
numObj.addItem(10);
let strObj = new customArray<string>();
strObj.addItem(“Robin”);
console.log(numObj);
console.log(strObj);
After running ts-node
, this is the output you will receive:
OutputcustomArray { arr: [ 10 ] }
customArray { arr: [ 'Robin' ] }
You used the customArray
class for both number
and string
types. You were able to accomplish this by using generic types. However, using generics does have some constraints. This will be discussed in the next step.
Up until this point, you’ve created functions and classes using generics. But there is a drawback to using generics. To see this drawback in action, write a function called getLength
that will return the length
of the function’s argument:
function getLength<T>(args: T) : number {
return args.length;
}
This function will work as long as the passing type has a length
property, but data types that don’t have a length
property will throw an exception.
There is a solution to this problem—creating generic constraints. To do this, you will first need to create an interface called funcArgs
and define a length
property:
interface funcArgs {
length: number;
}
Now, change the getLength
function and extend
it to include the funcArgs
interface as a constraint:
function getLength<T extends funcArgs>(args:T) : number {
return args.length;
}
You’ve created a generic constraint using an interface. Furthermore, you also extended the getLength
function with this interface. It now needs length
as a required parameter. Accessing this getLength
function with an argument that doesn’t have a length parameter will show an exception message.
To see this in action, declare a variable called result4
and assign it to getLength
with 3
as its argument:
let result4 = getLength(3);
This will return an error since a value for the length
parameter is not included:
Output⨯ Unable to compile TypeScript:
index.ts:53:25 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'funcArgs'.
53 let result4 = getLength(3);
To call the getLength
function, you will need to include a length
argument along with a name
argument:
let result = getLength({ length: 5, name: 'Hello'});
This is the right way of calling our function. This call has a length
property, and your function will work well. It will not show any error message.
In this tutorial, you successfully integrated generics into your TypeScript functions and classes. You also included constraints for your generics.
As a next step, you may be interested in learning how to use TypeScript with React. If you would like to learn how to work with TypeScript in VS Code, this How To Work With TypeScript in Visual Studio Code article is a great place to start.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!