TypeScript Generics Tutorial

In this TypeScript tutorial we learn about Generics and how they allow us to define placeholders for class and function types.

We cover how to both Generic classes and functions, multiple Type Parameters and how to constrain generics with interfaces.

What are Generics

Generics in TypeScript are basically class and function templates. A Generic allows us to define placeholders for its types.

As an example, let’s consider two functions that return some data. In some situations we need the function to return a number, and in other situations the type must be a string.

Example:
// Number type
function logNum(msg: number): number {
  return msg
}

// String type
function logString(msg: string): string {
  return msg
}

let result1 = logNum(30)
let result2 = logString("Hello World")

console.log(result1)
console.log(result2)

Instead of having two methods, we can define the function as a generic type and let the developer choose which type they need by specifying it in the function call.

Example:
// Definition
function logger<T>(msg: T): T {
  return msg
}

// Usage
let result1 = logger<number>(30)
let result2 = logger<string>("Hello World")

console.log(result1)
console.log(result2)

In the example above, the function is defined as generic, represented by T .

Of course TypeScript does have the any type which also technically makes the function generic.

Example:
// Definition
function logger(msg: any): any {
  return msg
}

// Usage
let result1 = logger(30)
let result2 = logger("Hello World")

console.log(result1)
console.log(result2)

The catch here is that the function can now accept any type of data, so we lose type safety.

How to define a Generic function

To define a Generic function, we need to use Type Parameters.

A Type Parameter is defined between the less than and greater than operators <> (open and close angle brackets), after the function name.

Example:
function_name<TYPE>

While the Type parameter can be anything, the convention is to use a capital T .

Example:
function_name<T>

If the function returns a value, we must also specify the same Type Parameter because the compiler has to assume that the type parameters could be used with any type.

In this context we write the Type Parameter without the open and close angle brackets.

Example:
function_name<T>(): T

If a function parameter needs the same type, we also specify it without the open and close angle brackets.

Example:
function_name<T>(param: T): T

As an example, let’s create a function that logs something to the console when invoked.

Example:
function logger<T>(msg: T): void {
  console.log(msg)
}

This time, the logger function logs directly to the console. It doesn’t return anything so we can let the return type be void .

What type the function will log to the console is important though. The ‘msg’ parameter can be of multiple types so it will hold the same Type Parameter as the function.

How to call (invoke) a Generic function

A generic function is invoked like a normal function, except that the type is specified between open and close angle brackets.

Let’s use our logger() function as an example.

Example:
// Definition
function logger<T>(msg: T): void {
  console.log(msg)
}

// Call
logger<number>(30)
logger<string>("Hello there")

The msg parameter will use the type that’s specified between angle brackets in the function call, because it uses the same Type Parameter in the definition.

Output:
30
Hello there

How to use multiple Type Parameters in a Generic function

To use multiple Type Parameters in a Generic function, we simply separate them with a comma.

As an example, let’s add another Type Parameter to our function.

Example:
// Definition
function log<T, U>(msg: T, msg2: U) {
  console.log(msg)
  console.log(msg2)
}

// Usage
log<number, string>(30, "Hello there")
log<string, boolean>("Greetings", true)

Once we define the second Type Parameter, U , we can use it somewhere in the function. In this case we just use it to log another message.

In the function call we add the second type between angle brackets, separated by a comma.

Output:
30
Hello there
Greetings
true

How to create a Generic class

As mentioned before, classes can also be Generic. As with functions, we specify a Type Parameter between open and close angle brackets, which can then be used throughout the class in methods and properties.

As an example, we will create a generic class that generates an array of a specified type.

First, let’s create a generic class and an empty generic array that uses the same Type Parameter as the class.

Example:
class CustomArray<T> {
  private arr: T[] = []
}

Next, we will create a method that will allow us to add items to the array.

Example:
class CustomArray<T> {
  private arr: T[] = []

  // Add array element
  addElement(item: T) {
    this.arr.push(item)
  }
}

In the addElement() method, the item parameter must have the same Type Parameter as the array itself because arrays can only hold values of a single type.

Our simple Generic class is done. We can now create a new object from it and specify the type between angle brackets after the class name.

Example:
class CustomArray<T> {
  private arr: T[] = []

  // Add array element
  addElement(item: T) {
    this.arr.push(item);
  }
}

// Instantiate
let employee = new CustomArray<string>();

In this case we create an object of type string because we want to add some names to the array.

Let’s also log the array to the console.

Example:
class CustomArray<T> {
  private arr: T[] = []

  // Add array element
  addElement(item: T) {
    this.arr.push(item)
  }
}

// Instantiate
let employee = new CustomArray<string>()

// Add elements
employee.addElement("John Doe")
employee.addElement("Jane Doe")

// Log array to console
console.log(employee)

That’s all there is to Generic classes, it’s quite easy.

Output:
CustomArray { arr: [ 'John Doe', 'Jane Doe' ] }

Generic constraints with Interfaces

As mentioned before, Generics allow any data type. However, we can restrict it to certain types using constraints.

As an example, let’s consider a function that draws a shape, like a circle or rectangle. We don’t want it to draw a text message, or a number, so we constrain it to just draw a shape.

We apply a constraint to a generic by using an Interface , then extending it from the Type Parameter with the extends keyword.

Example:
interface Shape {
  draw(): void
}

// Constrain T to be only of Shape
function drawShape<T extends Shape>(shape: T) {
  shape.draw()
}

The T Type Parameter can only accept a value that is of type Shape.

If we try to pass a string type to the method, TypeScript will raise an error.

Example:
interface Shape {
  draw(): void
}

// Constrain T to be only of Shape
function drawShape<T extends Shape>(shape: T) {
  shape.draw()
}

drawShape<string>("Error")
Output:
Type 'string' does not satisfy the constraint 'Shape'.

We have to pass a Shape type for the funtion to work.

To keep things simple, we will just log a message to the console and not actually draw a shape.

Example:
interface Shape {
  draw(): void
}

// Constrain T to be only of Shape
function drawShape<T extends Shape>(shape: T) {
  shape.draw()
}

class Circle implements Shape {
  draw() {
    console.log("Drawing circle")
  }
}

class Rectangle implements Shape{
  draw() {
    console.log("Drawing rectangle")
  }
}

let circle = new Circle()
let rectangle = new Rectangle()

drawShape<Shape>(circle)
// or
drawShape(rectangle)

Because the type is constrained, we don’t have to add the type explicitly in the function call.

This depends on your personal preference, but whichever one you use, stay consistent.

Output:
Drawing circle
Drawing rectangle

Summary: Points to remember

  • Generics in TypeScript allow us to define placeholders for class and function types.
  • Generics use Type Parameters to define types.
    • Type Parameters are conventionally written as a capital T .
    • Type Parameters on classes and functions are wrapped in open and close angle brackets.
    • Type Parameters on class and function members, parameters and return types are not wrapped in angle brackets.
    • Multiple Type Parameters can be used and is conventionally named as letters of the English alphabet, following T.
  • Generic types can be constrained by the Type Parameter extending an Interface.