Keeping Your TypeScript Code DRY With Generics
JavaScript has become popular over the years, with the increase in popularity you have more developers migrating from other strongly typed programming languages complaining about the loosely-typed nature of JavaScript and that brings us to TypeScript.
TypeScript is an open-source programming language that is a superset of JavaScript. It builds on top of JavaScript by adding statically typed definitions. This gives developers assurance over code written as it saves time catching errors before a code is executed.
In this article, we will be looking at one of the important concepts of TypeScript called Generics and how it helps developers in following the Don’t-Repeat-Yourself (DRY) principle.
This article assumes you have a basic knowledge of TypeScript to follow along. You can brush up on the basics here. You also need the following tools below to install TypeScript on your machine:
- Node v12 or greater
- npm v5.2 or greater
- A code editor
- A Terminal
Installing TypeScript
To get started with TypeScript open a terminal and run the code below to create a directory for the project:
mkdir typescript-generics-example
Navigate to the project directory
cd typescript-generics-example
Run the code below to create a package.json
file
npm init -y
Install the TypeScript library by running the code below
# using npm
npm install --global typescript # Global installation
npm install --save-dev typescript # Local installation
Create a tsconfig.json
file
tsc --init
The command above creates a tsconfig.json
file in your project directory. The tsconfig.json
file specifies the root files and the compiler options required to compile the project.
To test our setup create a file app.ts
in the root directory and paste the code below
function showMessage(name: string) {
console.log('Hello ' + name);
}
showMessage('Sama');
Now go to package.json
and modify the script
property to be similar to the code below
"scripts": {
"dev": "tsc app.ts && node app.js && rm -rf app.js"
},
The dev
command transpiles TypeSript to JavaScript before executing and then removes the JavaScript file after execution.
Open a terminal, navigate to the root directory of the project and run the code below to run the script
npm run dev
You should get “Hello Sama” as output in your terminal.
What are Generics?
Generics have been a major feature of strongly typed languages like Java and C#. In TypeScript, they allow the types of components and functions to be specified later which allows them to be used in creating reusable components that can apply to different use cases, for example:
function returnInput <Type>(arg: Type): Type {
return arg;
};
const returnInputStr = returnInput<string>('Foo Bar');
const returnInputNum = returnInput<number>(5);
console.log(returnInputStr); // Foo Bar
console.log(returnInputNum); // 5
In the example above, I created a generic function that returns the user input. When calling the function I made sure it is type-safe by specifying the type at the point of execution. This way the function is reusable for a different use case but also checks to make sure the type specified is returned.
The Problem
There are times when you want to create a function or component for multiple use cases like the example below:
type TodoItem = { taskId: number; task: string | number; done: boolean };
let id: number = 0;
let todoList: Array<TodoItem> = [];
function printTodos(): void {
console.log(todoList);
}
function addTodo(item: string): void {
todoList.push({ taskId: id++, task: item, done: false });
}
function addTodoNumber(item: number): void {
todoList.push({ taskId: id++, task: item, done: false });
}
addTodo('Learn TypeScript');
addTodoNumber(22);
printTodos();
In the code snippet we are trying to add two different types of todo items to the todoList
array, because of the different types we have to create two different functions to add todoItems
to the todoList
. This violates the Don’t-Repeat-Yourself (DRY) principle which states that “Every piece of knowledge or logic must have a single, unambiguous representation within a system”. In this case, the two functions have the same implementation with just different types.
An alternative to duplication can be done by using the any
type
type TodoItem = { taskId: number; task: string | number; done: boolean };
let id: number = 0;
let todoList: Array<TodoItem> = [];
function printTodos(): void {
console.log(todoList);
}
function addTodo(item: any): void {
todoList.push({ taskId: id++, task: item, done: false });
}
addTodo('Learn TypeScript');
addTodo(22);
printTodos();
This makes sure we adhere to the DRY principle but introduces another problem. Using any
datatype means we accept any type, which in turn means we aren’t controlling the type accepted and returned, thus invalidating the benefits of using types in our code.
In the next section, we will look at how to adhere to the DRY Principle by making sure our solution is generic and type-safe by controlling the type of data accepted and returned.
Using Generics to solve the problem
Now that we’ve established what Generics are and why they are needed let’s see how we can use them to solve the problem stated in the previous section.
let id: number = 0;
let todoList = [];
function printTodos(): void {
console.log(todoList);
}
function addTodo<Type>(item: Type): void {
todoList.push({ taskId: id++, task: item, done: false });
}
addTodo<string>('Learn TypeScript');
addTodo<number>(22);
printTodos();
In the solution above we’ve made the function addTodo
we created earlier, Generic. The <Type>
is a placeholder that will be replaced by the type when we run the function and it ensures the function is type-safe e.g addTodo<string>('Sama');
declares the type as a string so if the parameter passed to the function isn’t a string it’ll result in an error.
Generic functions also allow us to pass default types to the function definition. This helps define a default behavior if we want to avoid having to declare the type every time we use the function.
function addTodo<Type = string>(item: Type): void {
todoList.push({ taskId: id++, task: item, done: false });
}
addTodo('Learn TypeScript');
So far, we’ve looked at creating generic functions with just one parameter. What if the function has multiple parameters of different types?
let id: number = 0;
let todoList = [];
function printTodos(): void {
console.log(todoList);
}
function addTodo<T, S>(item: T, status: S): void {
todoList.push({ taskId: id++, task: item, done: status });
}
addTodo<string, boolean>('Learn TypeScript', true);
addTodo<number, boolean>(22, false);
printTodos();
We can solve this by passing multiple parameters as placeholders <T,S>
to the generic function definition and then specifying what type belongs to an argument (item: T, status: S)
. Lastly, when we run the function we can now replace the placeholders defined with the correct types addTodo<string, boolean>('Learn TypeScript', true)
.
Generic Classes
We can also make classes Generic. Let’s create a Todo
class to show how this works
let id: number = 0;
type TodoListItem = {
taskId: number;
task: string;
done: boolean;
}
class Todo<Type> {
_todoList: Array<Type> = [];
addTodo(item: Type): void {
this._todoList.push(item);
}
printTodos(): void {
console.log(this._todoList);
}
}
const Todos = new Todo<TodoListItem>();
Todos.addTodo({ taskId: id++, task: 'learn TypeScript', done: true });
Todos.addTodo({ taskId: id++, task: 'Practice TypeScript', done: false });
Todos.printTodos();
Generic classes are similar to Generic functions the major difference is in how they are used. Generic classes take a type parameter when they are instantiated new Todo<TodoItem>();
while Generic functions take a Type parameter at the point of execution:addTodo<string>('Sama')
.
Generic Constraints
While Generic functions and classes make sure we adhere to the DRY principle it also gives rise to some questions we will look at below:
- What if we want to limit a Generic function to certain types?
When a function is Generic it means it can be used in any form and also accept any type. For example
addTodo<boolean>(true);
But what if we want to use the generic function for strings and numbers but not boolean. How do we go about it?
To do so we’ll use the extends
keyword and specify the types we want to constrain the function to as specified in the example below
function addTodo<Type extends string | number>(item: Type): void {
todoList.push({ taskId: id++, task: item, done: false });
}
From the solution above we’ve made sure this generic function will only accept types of number and string and nothing else. This means when you try to execute the boolean example showed it’ll result in an error as boolean wasn’t part of the type constraint.
- What if our function implementation doesn’t support a particular Type?
Let’s imagine we have a function that returns the length of the todoList
function getTodoListLength<Type>(arr: Type): void {
console.log(arr.length);
}
If we try to execute this function with types that don’t have support for the length
attribute (like number
) it will result in an error.
To solve this problem we can also make use of the extends
keyword to constrain the types to what the implementation supports as shown below
function getTodoListLength<Type extends Array<TodoItem>>(arr: Type): void {
console.log(arr.length);
}
getTodoListLength<Array<TodoItem>>(todoList);
Measuring front-end performance
Monitoring the performance of a web application in production may be challenging and time-consuming. OpenReplay is an open-source session replay stack for developers. It helps you replay everything your users do and shows how your app behaves for every issue. It’s like having your browser’s inspector open while looking over your user’s shoulder.
OpenReplay lets you reproduce issues, aggregate JS errors and monitor your app’s performance. OpenReplay offers plugins for capturing the state of your Redux or VueX store and for inspecting Fetch requests and GraphQL queries.
Happy debugging, for modern frontend teams - Start monitoring your web app for free.
Conclusion
In this article, we’ve looked at how to use TypeScript Generics to write reusable functions and classes that respect the DRY principle. We’ve also looked at how to get around Generic constraints by using the extends
keyword.
While Generics might be difficult to understand the aim of this article was to make it less complex by using relatable examples so you’ll be able to practice and apply them in your applications.