Typescript Generics

« Return to the Chapter Index

Table of contents
  1. Typescript Generics
    1. Key Idea
    2. Generics in Typescript
    3. Motivation
    4. Generic Functions
      1. Controlling types
    5. Generic Classes
      1. Default Types
    6. Inside the Webz Notifier class
    7. Summary
  2. Next Step

Key Idea

Generics allow for creation of reusable code that where internal types can be specified externally.

Generics in Typescript

In the last chapter we discussed the Webz Notifier class. This class was a generic class that we could pass type parameters to during creation.

	event:Notifier = new Notifier();
	event2:Notifier<number> = new Notifier<number>();
	event3:Notifier<string> = new Notifier<string>();
	event4:Notifier<SomeClass> = new Notifier<SomeClass>();
	event5:Notifier<string[]> = new Notifier<string[]>();

This is a single class definition that works on any type of data. We can make our own generic functions, classes, interfaces, or type aliases by creating them with one or more type parameters that can be specified by the caller. Overall, this allows us to create reusable code that works on various types of data.

Motivation

Consider the following simple method.

function printNumberResult(result:number){
	console.log('Result: ' + result);
}
printNumberResult(5);

This method prints Result: 5 when called with a parameter of 5. What if we wanted to allow other types of data to be printed? One solution would be to write another function.

function printStringResult(result:string){
	console.log('Result: ' + result);
}
printStringResult("Hello World");

While we could write different functions for each type we wish to support, it would be better if we could right a single method for all of them. Let’s examine this code further:

Generic Functions

We know console.log(...) will print anything, so the only issue here is that our method expects a number. We can make this function a generic by adding a type parameter and using it as the type of the result parameter.

function printResult<T>(result:T){
	console.log('Result: ' + result);
}
printResult<number>(5);
printResult<string>("Hello World");

Here we have added a type parameter (T), and we use that paramter to set type type of the function’s parameter (result). When we call our function, we can specify the type of the data when we call it.

It turns out that typescript can infer the type from the parameter, so we can leave it out when we call the function (However it is not incorrect to include it).

function printResult<T>(result:T){
	console.log('Result: ' + result);
}
printResult(5);
printResult("Hello World");

We are not limited to a single type parameter. If we need more than one, we can specify multiple type parameters.

function makePair<T,S>(x:T,y:S):[T,S]{
	return [x,y];
}
const result=makePair<number,string>(1,"hello");
console.log(result);

The important point here is that the type checking occurs at compile time (not at run time). If we call it with the wrong arguments…

function makePair<T,S>(x:T,y:S):[T,S]{
	return [x,y];
}
const result=makePair<number,string>("hello“,1);
console.log(result);

… you will get compiler errors. Try it and you will see the errors in the console.

It is much easier to fix compiler errors where the compiler gives us a line number and description then it is to fix run time errors where the program either crashes, or just gives the wrong answer.

Controlling types

We can limit the types that are acceptable as a type parameter by using the extends keyword. In this example, the first parameter must be a string or a number, but the second parameter can be any type.

function makePair<T extends string|number,S>(x:T,y:S):[T,S]{
	return [x,y];
}
console.log(makePair(4,["Hello"]));

Note: string|number is referred to as a Union Type which we will talk more about later, but basically we can combine types with a | and then either type would be acceptable.

If we use extends with a class type, we could use elements of that class or any class that derives from the class specified in the type paramter’s extends clause.

class Shoe{
	constructor(public size:number){}
}
class Sneaker extends Shoe{
	constructor(size:number,private sport:string){
		super(size);
	}
}
class Boot extends Shoe{
	constructor(size:number,private height:number){
		super(size);
	}
}
function getShowSize<T extends Shoe>(shoe:T):number{
	return shoe.size;
}
console.log(getShowSize(new Boot(10,14)));

Note: We could do this without a generic if we made the parameter type Shoe as it would accept the derived classes. In this case either method is ok, but there are places where a generic is a better solution.

Generic Classes

Just like functions, we can use generics for classes as well. Let’s consider a class for a list of numbers:

class ItemList{
	public items:number[]=[];
	constructor(){}
	addItem(item:number):void{
		this.items.push(item);
	}
}
const list:ItemList=new ItemList();
list.addItem(4);
console.log(list);

What if we wanted to extend this so it worked on a list of any type, even a list of lists. We could use a generic definition to make ItemList work on any type, and not just on numbers As always we can limit the acceptable types using the extend keyword.

class ItemList<T>{
	public items:T[]=[];
	constructor(){}
	addItem(item:T):void{
		this.items.push(item);
	}
}
const list:ItemList<number>=new ItemList<number>();
list.addItem(4);
const list2:ItemList<string>=new ItemList<string>();
list2.addItem("hello");
const list3:ItemList<string[]>=new ItemList<string[]>();
list3.addItem(["Hello","World"]);
console.log(list);
console.log(list2);
console.log(list3);

Note: T is defined on the class, and we can use it within the class as the type of any method parameter, return value, or member variable.

We can create a homogeneous list of anything by specifying the type of object the list contains with a type parameter. Now we have created a class that works on any data, instead of just on numbers. We can even add additional type parameters to the methods within our class to make them more reusable.

Default Types

Finally, we can provide a default value for our generic to describe how it behaves if no type parameter is provided:

class ItemList<T=number>{
	public items:T[]=[];
	constructor(){}
	addItem(item:T):void{
		this.items.push(item);
	}
}
const list:ItemList=new ItemList();
list.addItem(4);
const list2:ItemList<string>=new ItemList<string>();
list2.addItem("hello");
const list3:ItemList<string[]>=new ItemList<string[]>();
list3.addItem(["Hello","World"]);
console.log(list,list2,list3);

If a parameter is provided, the default is ignored. If no parameter is provided, then the type must match the default if we use the class (i.e. we must pass a number, anything else will cause a type error at compile time).

Inside the Webz Notifier class

Let’s return to the Webz Notifier class and loot at the source code for it.

export class Notifier<T = void> {
	constructor() {}
	subscribe(callback: (value: T) => void, error?: (value: Error) => void) {
		//something goes here
	}
	unsubscribe(id: number) {
		//something goes here
	}
	notify(value: T) {
		//something goes here
	}
	error(value: Error) {
		//something goes here
	}
}

*T defaults to void if no parameter is provided.

  • subscribe takes a function whose parameter has type T.
  • notify takes a value of type T

This is as expected when you consider how we used Notifier previously.

  • With no type argument its data is void (nothing)
  • With a type parameter, the type it works with is the value specified for T.

Summary

Using generics, we can create more reusable code by allowing our code to work on many different types of data. We can apply this techinque to classes and methods so that our code works on various types of data.

Next Step

Next we’ll learn about interfaces Typescript Interfaces »


Back to top

Created by Greg Silber, Austin Cory Bart, James Clause.