Abstract: TypeScript is the evolution of JavaScript and its language services are far superior to any other JavaScript tool in existence today. Generics are a fantastic way to demonstrate this powerful advancement for JavaScript development. In this tutorial, we'll focus on generics in TypeScript.

TypeScript is an amazing innovation with how we as developers write JavaScript applications.

TypeScript boasts its language services as a first-class citizen. These language services empower your favorite integrated development environment with advanced statement completion, refactoring, type-checking, compilation and even flow analysis.

In addition to all these capabilities, the language itself offers a more "grown up" approach to JavaScript development. As a superset of JavaScript, TypeScript really is the answer to many problems.

In this tutorial we'll focus on generics in TypeScript. We'll cover how TypeScript allows developers to leverage generics, the generic syntax, limitations with generics and other key considerations.

Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering Angular, TypeScript, React, Vuejs, .NET Core, MVC, Azure and more. Subscribe to this magazine for FREE and download all previous, current and upcoming editions.

Generics in TypeScript - Introduction

I've said it before and I'll say it again, software exists to aid with the movement and manipulation of data.

When it's stated like this – it's rather boring. While the context of the data can mean all the difference in how emotionally developers are connected to their source code, it is an art form we should all take pride in.

I'm an advocate of software craftsmanship and education. I believe that there is always a better way of doing something until you run out of time or priorities change. With this we consider JavaScript, a language prototyped in only ten days – which really explains a lot.

Despite negativity coming from the community about JavaScript, it is one of the world’s most powerful, prevalent and popular programming languages today. JavaScript runs nearly everywhere, and with such demand comes an opportunity for improvement.

Compile vs Transpile

Since we’re discussing craftsmanship and education, let us clear up some misconceptions. There is still a lot of confusion in our industry as it pertains to TypeScript compilation. There are those who will argue it is not compilation and those who believe it's only transpiled.

The act of transpiling your TypeScript into JavaScript, is referred to as compilation. You are in fact compiling your TypeScript code into JavaScript.

The term "transpile" is a derivative of "compile". The distinction between the two has to do with the level of abstraction, i.e. when you compile C# into IL, the code is seemingly unrecognizable from its original source – as it has been so heavily abstracted. Using this understanding, let's apply this notion to TypeScript.

The resulting JavaScript from a TypeScript compilation varies depending on the ECMAScript version you're targeting – there is an inverse correlation between the level of abstraction and the target ECMAScript version.

If you're targeting ESNext in your tsconfig.json, which supports the latest proposed ES features – the level of abstraction in compiling TypeScript into JavaScript is significantly smaller than if you were targeting ES3. As such it would be fair to refer to this as "transpiling"; however, it is always safe to call this a "compilation" because transpiling is a form of compiling.

It is not necessarily incorrect for someone to use these interchangeably – although one might be more accurate.

Generic Syntax

In TypeScript, the syntax for generics is rather simple. We use angle brackets to enclose the generic type-parameter.

Let's take the name "TypeScript" for example, imagine that we have a generic class named Script and we constrain the generic type-parameter to extend from Type. We would end up with the following:

type Type = number | Date | string; class Script<T extends Type> { constructor( public content: T, public name: string) { } }

This simple class has two properties, content and name.

Notice that name is a string and content is of type T. Since we have defined the constructor parameter for content as a type of T, this Script class takes on the shape of anything that satisfies its type constraint.

In this case, T must extend Type and Type is defined as a type alias. The type alias is a union of number, Date and string. This means that T must be one of those types.

Let's instantiate a new Script object instance with a Date as its parameter type argument.

const script = new Script<Date>(new Date(), "Date object."); const date = script.content;

The value of the content member is of type Date and the language services enforce and recognize this as the only type for this member. If we try to reassign the content value of this instance to a different type, we will get an error.

It is important to note that we are even attempting to reassign the value to one of the accepted types we are constrained to, number.

This does not matter.

The constraint is applied when calling the constructor and lives for the life of the instance and cannot be changed afterwards. When we instantiate a Script object, the type argument is actually optional since it is inferred by the type passed into the constructor. As such we can simplify our declaration:

const script = new Script(new Date(), "Date object."); const date = script.content;

But it doesn’t hurt to be explicit.

This is more of a stylistic decision that perhaps is best mutually decided by the development team. I personally prefer being implicit as the code is less verbose and more closely resembles JavaScript.

Constraints

At its most pure and simple form, generic syntax for TypeScript is effectively a constraint on a parameter type as was demonstrated above.

This is familiar to developers who are accustomed to languages such as C# that have supported generics for years now. Although it is familiar in semantics, the syntax does differ. Instead of defining a type alias we’ll have a look at interfaces.

interface Sport { name: string; association: string; yearFounded: number; getNumberOfTeams(): number; }

We now have a basic interface describing a Sport. TypeScript allows you to define, declare and use interfaces.

However, interfaces do not exist in JavaScript.

When your TypeScript code is compiled to JavaScript, the interfaces go away…they exist solely to empower the language services to reason about the shape of your objects. Likewise, they can be used for generic type constraints.

Our first implementation of this interface will be the Football object.

class Football implements Sport { public yearFounded = 1920; public name = 'Football'; public association = 'NFL'; public tags = [ 'Pigskin', 'American Football', 'Gridiron' ]; public getNumberOfTeams() { return 32; } }

For the sake of having multiple implementations we will also introduce a Basketball implementation of our Sport interface.

class Basketball implements Sport { public yearFounded = 1946; public name = 'Basketball'; public association = 'NBA'; public description = 'Boring game, only excitement is the final minute of play!'; getNumberOfTeams() { return 30; } deleteFromExistence(really: boolean = true): void { // If only it were so easy... } }

We’ll now leverage a generic lambda to output a message that relies on the Sport interface.

const sportWriter = <TSport extends Sport>(sport: TSport) => { if (sport) { const teamCount = sport.getNumberOfTeams(); console.log( `${sport.name} the sport, better known as the ` + `(${sport.association}) has ` + `${teamCount} teams and was founded in ${sport.yearFounded}.`); } } sportWriter(new Football());

The following will output as follows:

Football the sport, better known as the (NFL) has 32 teams and was founded in 1920.

The benefits of this use of generics are simple, sportWriter will only accept implementations of Sport.

Generic Structures

In addition to type parameter constraints, we can build out generic lists and similar data structures that do not rely on constraints – they are simply generic to any type.

class List<T> { private items: T[] = []; public add(value: T): this { this.items.push(value); return this; } public remove(value: T): this { let index = -1; while (this.items && this.items.length > 0 && (index = this.items.indexOf(value)) > -1) { this.items.splice(index, 1); } return this; } public toString(): string { return this.items.toString(); } }

This represents a generic list, wherein it can contain any single type. This is extremely powerful and ensures type-safety. This means that if a list is instantiated with a type parameter argument of type string, the list can only allow the consumer to add and remove string values.

const list = new List<string>(); list.add("David") .add("Michael") .add("-") .add("Michael") .add("-") .add("Michael") .add("-") .add("Pine") .remove("-"); console.log(list); console.log(list.remove("Michael"));

This code will add a series of strings to the underlying array via the add API. Notice that we allow for a sense of functional programming by returning the instance itself after each function call – this enables a fluent API, where we can chain method invocations seamlessly together. Here’s the resulting execution output.

List { items: [ 'David', 'Michael', 'Michael', 'Michael', 'Pine' ] }

List { items: [ 'David', 'Pine' ] }

Generics - Benefits

Generics enable common code to be reused and make encapsulation more obvious.

An example of this is found during development of an application that performs common operations on different models. Often developers find existing code that acts similar to what they need. This leads to copying and pasting code, changing variables as needed - this is wrong!

Instead, developers should take this opportunity to encapsulate the common sequence, steps, or functionality found within the logic they were attempting to replicate – and make it generic if possible.

Doing so will encapsulate the common logic, thus reusing existing code.

Generics - Limitations

The generic syntax has a bit of a learning curve and it takes time to fully understand it.

The syntax itself can be off-putting at first, but again with time it will become obvious. The one true limitation is really just readability – developers unfamiliar with the syntax might have difficulty reading it initially.

However, it is easy to argue that the semantics of other languages that have adopted generic syntax are identical to how TypeScript has implemented this feature. Leveraging generic syntax is a decision the development team must embrace, but a decision nonetheless.

With this in my mind, it could be perceived as a limitation.

Conclusion

TypeScript is the evolution of JavaScript that the developer community is actively adopting.

The language services are far superior to any other JavaScript tool in existence today. Generics are a fantastic way to demonstrate this powerful advancement for JavaScript development. It’s a feature that will help developers reuse common code and is perfect for modularity.

I highly recommend that you start using TypeScript generics today!

Resources

https://www.typescriptlang.org/docs/handbook/generics.html

This article was technically reviewed by Damir Arh.

This article has been editorially reviewed by Suprotim Agarwal.