Getty Images/iStockphoto

Understanding TypeScript generics

Programming languages such as Java, C# and Swift use generics to create code that's reusable and enforce type safety. In this tutorial, learn how generics in TypeScript work.

Generics are a powerful feature of most programming languages. They help developers create functions, classes and interfaces in which specific types are declared later on in the programming lifecycle.

Strongly typed programming languages including Java, C# and Swift support generics. They are called templates in the C++ programming language, and lightweight generics in Objective-C. JavaScript, which is dynamically typed, does not support generics. TypeScript was created in part as a strongly typed option for JS programmers.

This article explains the essentials of generics in TypeScript. We'll discuss the syntax for using generics, and demonstrate how to use generics to enforce type safety at compile time. Also, you'll see an example of how to use generics to implement conditional logic in a function according to the type declared when using the generic. This assumes that the reader has a working knowledge of TypeScript programming and some familiarity of object-oriented programming, but is just getting started working with generics.

The syntax of generics

The key to working with the syntax of generics is to understand the concept of a generic variable. A generic variable is a single character symbol that indicates a data type to be declared later on. Think of a generic variable as a "type placeholder."

By convention, the symbol used for a generic variable is one of the uppercase characters: T, V, K, S or E. (You can use any alphabetic character you like, but an uppercase character makes your code easier to understand.) These characters are placed between opening and closing "angle brackets, <like so>." Then, to use the generic, the programmer supplies the actual type name for each generic variable declared.

As mentioned previously, developers can use generics with functions, interfaces and classes. The sections that follow describe the syntax and usage of each.

Syntax of function with one parameter

Example 1 shows a function named myFunctionOne<T>() that uses a generic variable T to indicate the type of the parameter named param1.

function myFunctionOne<T>(param1: T): void {
    console.log(`The type of the parameter value ${param1} is: ${typeof param1}`)
}

Example 1: A function that declares a single generic variable, T.

Notice that the generic variable T is placed between angle brackets in the function signature, like so: myFunctionOne<T>.

The reason to put the <T> declaration with the function name is that it defines the generic variable that will be used in the function, as in this case with the parameter declaration.

The following code snip shows the function myFunction<T>() declaring a type of string for the value of T. Subsequently, the type of the parameter value passed in the function is a string.

myFunctionOne<string>("Hi There!");

This is the output:

The type of the parameter value Hi There! is: string

Syntax of function with two parameters

Example 2 below shows a function named myFunctionTwo<T, V>() that uses two generic variables T and V to indicate the types that the function will use. In this case, the generic variables T and V define the types for param1 and param2, respectively.

function myFunctionTwo<T, V>(param1: T, param2: V): string {
    return JSON.stringify({ param1, param2 })
}

Example 2: A function that declares two generic variables, T and V.

Here's a more specific use case:

const str = myFunctionTwo<number, string>(100, "People");
console.log(str);

And the output should look like this:

{ "param1": 100, "param2": "People" }

Interface syntax

Example 3 shows a TypeScript interface that declares a generic variable named T, which is used to define the type for the data member named value.

interface IGenericInterface<T> {
   value: T;
}

Example 3: A TypeScript interface that declares a generic variable, T.

The code below shows how a TypeScript class implements the interface IGenericInterface. Notice that the usage of IGenericInterface sets the value of the implicit generic variable T to a string, as indicated in the expression IGenericInterface<string>. Thus, the type of the class member value is also implicitly set to the type string.

class MyClass implements IGenericInterface<string>{
    value: string = "Hi There";
}
const message = new MyClass();
console.log(message.value)

And here's the simple output:

Hi There

Class syntax

Example 4 below shows a class named GenericClass<T, V>. The class declares two generic variables T and V that can be used within the class. Notice that the generic variable T defines the type of the class member named make. The generic variable V defines the type of the class member named model.

Also, notice that the class's constructor takes two parameters named make and model. The values of these parameters get assigned to the class members. The thing to remember is that the generic variables T and V are available to the constructor's parameters because they are defined in the class declaration like so: GenericClass<T, V>.

class GenericClass<T, V> {
    make: T;
    model: V

    constructor(make: T, model: V) {
        this.make = make;
        this.model = model;
    }
}

Example 4: A TypeScript class that declares two generic variables, T and V that the class uses.

Here are two different ways to apply the GenericClass<T, V> described above. The first defines the string type to both the implicit generic variables T and V:

const car1 = new GenericClass<string, string>("Kia", "Soul");
console.log(`${car1.make} ${car1.model}`)

And here's the output:

Kia Soul

The second example defines the string type for the T generic variable and the number type for the V generic variable:

const car2 = new GenericClass<string, number>("Chrysler", 300);
console.log(`${car2.make} ${car2.model}`

And here's the output:

Chrysler 300

Enforcing type safety

One of the benefits of using generics is that they enforce type safety in reusable code. One developer can create code that has behavior based on a generic variable, and then another developer can define the exact type for the generic variable. Once that type is defined, the code throws an error at compile time should it use anything but the defined type.

To illustrate this, let's look at the TypeScript Array object, which supports generics implicitly. Those that already use TypeScript have probably come across the following declaration for an array:

const arr = new Array<string>()

This statement is saying, "Make an array in which only data of type string can be added to the array." Thus, the following statement will work:

arr.push("Hi There")

While this statement will raise a compile time error as follows:

arr.push(1)

Why? The array has been declared to only allow the addition of data of the type string. The second code snip uses an integer, hence the error.

Under the covers, the generic definition of that array is the following:

Array<T>

This T is a generic variable that represents a type that will be declared later.

Again, the nice thing about the Array object's support for generic declaration is that errors are thrown at compile time. Also, many development tools such as Visual Studio Code and WebStorm report the type mismatch error as you write the code, as shown in the screenshot below.

IDE flags a TypeScript type mismatch error.
An error raised in an IDE due to a type mismatch.

Using generics makes the code self-correcting. This is nice in terms of programming magic, but the real question at hand is this: Why would we want to ensure type safety in an array?

Consider a program that requires an array that contains only strings -- for example, the 12-word authentication phrase published by a cryptocurrency wallet such as MetaMask. Under no circumstance do we want even the possibility to add something other than a string to the array, even at programming time. Thus, we declare the array like so:

const seedPhrase = new Array<string>()

This ensures that the programmer working with the code can never add anything other than a string to the array. The generic enforces type safety.

Conditional behavior based on type

Another benefit of generics is that developers can program conditional behavior based on the type that is assigned to a generic variable.

Example 5 below shows a class named SmartPrinter<T> that has a method named print(data: T). The generic variable T is defined in the class declaration at Line 1, but is used in the print method at Line 2.

1 export class (SmartPrinter<T> { 
2 print(data: T) {
3 if (typeof data === "string") {
4 console.log(`I am going to print: ${data.toLocaleUpperCase()}`);
5 } else {
6 console.log(`I am going to print a ${typeof data}`);
7 }
8 }
9 }

Example 5: A class that uses a generic variable and executes conditions logic based on the generic variable's type.

Notice that the print(data: T) method has behavior that inspects the actual type of data parameter at Lines 3 - 7. The inspection occurs at run time, and the code behaves accordingly. The following shows various ways of using the SmartPrinter.print(data: T) method with the associated results.

Here's an example of how to code this:

const smartPrinter = new SmartPrinter()
smartPrinter.print(1);
smartPrinter.print("This is very cool");
smartPrinter.print({ firstName: "Joe", lastName: "Jones" });

The output is as follows:

I am going to print a number
I am going to print: THIS IS VERY COOL
I am going to print a object

As you can see, the ability to add conditional behavior based on specific types introduces a new dimension in generics programming. Granted, it's a somewhat advanced topic, but it's good to know that you can do conditional programming in terms of generics.

Moving forward

TypeScript generics is a broad topic, and this is only the beginning. This area can become quite complex as you advance with type programming. Nonetheless, this article is a good place to start working with generics, and offers insight into the work of those developers who created them. As you learn more, you'll be able to create generic code that in turn will help other programmers. It's win-win all around.

Dig Deeper on Core Java APIs and programming techniques