An intro to TypeScript generics

This blog is a part of my TypeScript series, and the previous ones are:

1. Why program in TypeScript
2. Structural vs nominal typing
3. Getting started with TypeScript classes
4. Access modifiers public, private, and protected
5. Abstract classes
6. enums

We know that TypeScript has built-in types and you can create the custom ones as well. But there’s more to it. Strange as it may sound, types can be parameterized, i.e. you can provide a type (not the value) as a parameter.

It’s easy to declare a function that takes parameters of specific concrete types e.g. a number and a string:

function calctTax(income: number, state: string){...}

But TypeScript generics allow you to write a function that can work with a variety of types. In other words, you can declare a function that works with a generic type(s), and the concrete type(s) can be specified later by the caller of this function.

In TypeScript, you can write generic functions, classes, or interfaces. A generic type can be represented by an arbitrary letter(s), e.g. T in Array<T>, and when you declare a specific array, you provide a concrete type in angle brackets, e.g. number:

let lotteryNumbers: Array<number>;

In this blog, you’ll learn how to use generic code written by someone else as well as how to create your own classes, interfaces, and functions that can work with generic types.

Understanding Generics

A generic is a piece of code that can handle values of multiple types that are specified at the moment of using this piece of code (e.g. function invocation or class instantiation). Let’s consider TypeScript arrays, which can be declared as follows:

  1. Specify the type of the array element followed by []:

    const someValues: number[];

  2. Use a generic Array followed by the type parameter in angle brackets:

    const someValues: Array<number>;

With the second syntax, the angle brackets represent a type parameter. You can instantiate this the Array like any other while restricting the type of allowed values, which is number in our example.

The next code snippet creates an array that will initially have 10 objects of type Person, and the inferred type of the variable people is Person[].

class Person{ }

const people = new Array<Person>(10);

TypeScript arrays can hold objects of any type, but if you decide to use the generic type Array, you must specify which particular value types are allowed in the array, e.g. Array<Person>. By doing this, you place a constraint on this instance of the array. If you were to try to add an object of a different type to this array, the TypeScript compiler would generate an error. In another piece of code, you can use an array with a different type parameter, e.g. Array<Customer>.

The code in the next listing declares a class Person, its descendant Employee, and a class Animal. Then it instantiates each class and tries to store all these objects in the workers array using the generic array notation with the type parameter Array<Person>.

class Person {  
    name: string;
}

class Employee extends Person {  
    department: number;
}

class Animal {  
    breed: string;
}

const workers: Array<Person> = []; 

workers[0] = new Person();  
workers[1] = new Employee(); 
workers[2] = new Animal(); // compile-time error

The last line won’t compile because the array workers was declared with a type parameter Person, and our Animal is not a Person. But the class Employee extends Person and is considered a subtype of a Person; you can use the subtype Employee anywhere where the supertype Person is allowed

So, by using a generic array workers with the parameter <Person>, we announce our plans to store only instances of the class Person or its subtypes there. An attempt to store an instance of the class Animal (as it was defined in the previous listing ) in the same array will result in the following compile-time error “Type Animal is not assignable to type Person. Property name is missing in type Animal.” In other words, using TypeScript generics helps you to avoid errors related to using the wrong types.

Note

The term generic variance is about the rules for using subtypes and supertypes in any particular place of your program. For example, in Java, arrays are covariant, which means that you can use Employee[] (the subtype) where the array Person[] (the supertype) is allowed.
Since TypeScript supports structural typing, you can use either an Employee or any other object literal that’s compatible with the type Person where the Person type is allowed. In other words, generic variance applies to objects that are structurally the same. Given the importance of anonymous types in JavaScript, an understanding of this is important for the optimal use of generics in Typescript.
To see if type A can be used where type B is expected, read about structural subtyping.

Tip

We used const (and not let) to declare the identifier workers because its value never changes in the above listing. Adding new objects to the array workers doesn’t change the address of the array in memory hence the value of the identifier workers remains the same.

If you’re familiar with generics in Java or C#, you may get a feeling that you understand TypeScript generics as well. There is a caveat, though. While Java and C# use the nominal type system, TypeScript uses the structural one as explained in this blog.

In the nominal system, types are checked against their names, but in a structural system, by their structure. In languages with the nominal type system, the following line would always result in an error:

let person: Person = new Animal();

With a structural type system, as long as the structures of the type are similar, you may get away with assigning an object of one type to a variable of another. Let’s add the property name to the class Animal, as seen below.

class Person {
    name: string;
}

class Employee extends Person {
    department: number;
}

class Animal {
    name: string; 
    breed: string;
}

const workers: Array<Person> = [];

workers[0] = new Person();
workers[1] = new Employee();
workers[2] = new Animal();  // no errors

Now the TypeScript compiler doesn’t complain about assigning an Animal object to the variable of type Person. The variable of type Person expects an object that has a property name, and the Animal object has it! This is not to say that Person and Animal represent the same types, but these types are compatible.

Moreover, you don’t even have to create a new instance of PersonEmployee, or Animal classes but use the syntax of object literals instead. Adding the following line to the above listing is perfectly fine because the structure of the object literal is compatible with the structure of type Person:

workers[3] = { name: "Mary" };

On the other hand, trying to assign the Person object to a variable of type Animal will result in the compilation error:

const worker: Animal = new Person(); // compilation error

The error message would read “Property breed is missing in type Person”, and it makes sense because if you declare a variable worker of type Animal but create an instance of the object Person that has no property breed, you wouldn’t be able to write worker.breed hence the compile-time error.

Note

The previous sentence may irritate savvy JavaScript developers who’re accustomed to adding object properties like worker.breed without thinking twice. If the property breed doesn’t exist on the object worker, the JavaScript engine would simply create it, right? This works in the dynamically typed code, but if you decided to use the benefits of the static typing, you have to play by the rules. When in Rome, do as the Romans do.

Generics can be used in a variety of scenarios. For example, you can create a function that takes values of various types, but during its invocation, you must explicitly specify a concrete type. To be able to use generic types with a class, interface, or a function, the creator of this class, interface, or function has to write them in a special way to support generic types.

 Open TypeScript’s type definition file (lib.d.ts) from the TypeScript GitHub repository at line 1008 and you’ll see the declaration of the interface Array, as shown below.

The <T> in line 1008 is a placeholder for a concrete type that must be provided by the application developer during the declaration of the array like we did earlier. TypeScript requires you to declare a type parameter with Array, and whenever you’ll be adding new elements to this array, the compiler will check that their type matches the type used in the declaration.

In our code samples, we used the concrete type <Person> as a replacement of the generic parameter represented by the letter <T>:

const workers: Array<Person>;

But because generics aren’t supported in JavaScript, you won’t see them in the code generated by the transpiler – generics (as any other types) are erased. Using type parameters is just an additional safety net for developers at compile time.

You can see more generic types T in lines 1022 and 1026 in the above figure. When generic types are specified with the function arguments, no angle brackets are needed, and you’ll see this syntax in the next listing. There’s no T type in TypeScript. The There means the push() and pop() methods let you push or pop objects of the type provided during the array declaration. For example, in the following code snippet, we declared an array using the type Person as a replacement of T and that’s why we can use the instance of Person as the argument of the method push():

const workers: Array<Person>;
workers.push(new Person());
Note

The letter T stands for type, which is intuitive, but any letter or word can be used for declaring a generic type. In a map, developers often use the letter K for key and V for value.

Seeing the type T in the API of the interface Array tells us that its creator enabled support of generics. In one of the future blogs, I’ll show you how to create your own generic types.  Even if you’re not planning to create your own generic types, it’s really important that you understand the syntax of generics while reading someone else’s code of TypeScript documentation. You can also read our book TypeScript Quickly, where it’s explained.

2 thoughts on “An intro to TypeScript generics

Leave a comment