Back to Course

Learn TypeScript

0% Complete
0/0 Steps
Lesson 17 of 25
In Progress

Declaring and Using Generics in Functions, Classes, and Interfaces

Generics are a powerful feature in TypeScript that allow you to create reusable components that can work with a variety of types. They are particularly useful when you need to write functions, classes, or interfaces that can work with multiple types, but you don’t want to have to write separate versions for each type.

In this article, we’ll look at how to declare and use generics in functions, classes, and interfaces in TypeScript.

Declaring Generics in Functions

To declare a generic in a function, you use the angle brackets <> and give the generic a name. For example:

function identity<T>(arg: T): T {
  return arg;
}

In this example, the function “identity” takes an argument of type T and returns a value of type T. The type T is a generic type parameter, and it can be any type.

To call the function, you can specify the type parameter by using the type in angle brackets:

let output = identity<string>('Hello'); 
// output: string

You can also leave out the type parameter and let TypeScript infer the type from the argument:

let output = identity('Hello'); 
// output: string

Declaring Generics in Classes

To declare a generic in a class, you use the same syntax as in a function. For example:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

In this example, the class “GenericNumber” has a generic type parameter T, which is used for the “zeroValue” and “add” properties.

Declaring Generics in Interfaces

To declare a generic in an interface, you use the same syntax as in a function or class. For example:

interface GenericIdentityFn {
  <T>(arg: T): T;
}

let myIdentity: GenericIdentityFn = function<T>(arg: T): T {
  return arg;
}

In this example, the interface “GenericIdentityFn” has a generic type parameter T, which is used for the argument and return type of the function.

Using Generics with Type Constraints

Sometimes you may want to specify a constraint on the types that can be used as the generic type parameter. To do this, you can use the “extends” keyword followed by a type.

For example, you may want to create a function that works with arrays, but only arrays of a certain type. You can do this by adding a type constraint to the generic type parameter:

function copyArray<T extends number | string>(arr: T[]): T[] {
  return arr.slice();
}

let numArr = [1, 2, 3];
let strArr = ['a', 'b', 'c'];

let numArrCopy = copyArray(numArr);  // numArrCopy: number[]
let strArrCopy = copyArray(strArr);  // strArrCopy: string[]

Using Generics with Class Types

You can also use generics with class types. For example, you can create a generic “Factory” class that creates instances of a specific class. To do this, you can use the “new” keyword and the “Class” type:

class Factory<T> {
  create(c: {new(): T;}): T {
    return new c();
  }
}

class Product {
  name: string;
}

let factory = new Factory<Product>();
let product = factory.create(Product);
product.name = 'Product 1';

In this example, the “Factory” class has a generic type parameter T, which is used to specify the type of the “create” method’s return value. The “create” method takes a class constructor as its argument, and it returns an instance of the class.

Conclusion

Generics are a powerful feature in TypeScript that allow you to create reusable components that can work with a variety of types. They are particularly useful when you need to write functions, classes, or interfaces that can work with multiple types, but you don’t want to have to write separate versions for each type.

In this article, we looked at how to declare and use generics in functions, classes, and interfaces in TypeScript. We also looked at how to use generics with type constraints and class types.

With this knowledge, you should be able to use generics effectively in your TypeScript projects to create more flexible and reusable code.

Exercises

To review these concepts, we will go through a series of exercises designed to test your understanding and apply what you have learned.

Write a generic function named “swap” that takes two variables and swaps their values.

function swap<T>(a: T, b: T): [T, T] {
  return [b, a];
}

let a = 1;
let b = 'b';
let c = true;

console.log(swap(a, b)); // Output: ['b', 1]
console.log(swap(b, c)); // Output: [true, 'b']
console.log(swap(a, c)); // Output: [true, 1]

Write a generic function named “findIndex” that takes an array and a value, and returns the index of the value in the array if it exists, or -1 if it does not.

function findIndex<T>(arr: T[], value: T): number {
  return arr.indexOf(value);
}

console.log(findIndex([1, 2, 3, 4, 5], 3)); // Output: 2
console.log(findIndex(['a', 'b', 'c', 'd'], 'b')); // Output: 1
console.log(findIndex([true, false, true], false)); // Output: 1
console.log(findIndex([1, 2, 3, 4, 5], 6)); // Output: -1

Write a generic class named “SortedArray” that implements a sorted array data structure. The sorted array should have the following methods: “add”, “remove”, and “search”. The array should always be sorted in ascending order.

class SortedArray<T> {
  private arr: T[] = [];

  add(value: T) {
    this.arr.push(value);
    this.arr.sort();
  }

  remove(value: T) {
    let index = this.arr.indexOf(value);
    if (index >= 0) {
      this.arr.splice(index, 1);
      return true;
    }
    return false;
  }

  search(value: T) {
    return this.arr.includes(value);
  }
}

let sortedArray = new SortedArray<number>();
sortedArray.add(5);
sortedArray.add(2);
sortedArray.add(1);
sortedArray.add(3);

console.log(sortedArray.search(1)); // Output: true
console.log(sortedArray.search(4)); // Output: false
console.log(sortedArray.remove(1)); // Output: true
console.log(sortedArray.remove(4)); // Output: false

Write a function named “unique” that takes in an array of values and returns an array with only the unique values. Use generics to make the function work with any type of value.

function unique<T>(arr: T[]): T[] {
  return [...new Set(arr)];
}

console.log(unique([1, 2, 3, 3, 4, 4, 4, 5, 5])); // Output: [1, 2, 3, 4, 5]
console.log(unique(['a', 'a', 'b', 'b', 'c', 'c'])) // Output: ['a', 'b', 'c']

Write a function named “groupBy” that takes in an array of values and a key extractor function, and returns an object with keys for each unique value returned by the key extractor function. Use generics to make the function work with any type of value.

function groupBy<T, K>(arr: T[], keyExtractor: (item: T) => K): {[key: K]: T[]} {
  let groups: {[key: K]: T[]} = {};
  for (let item of arr) {
    let key = keyExtractor(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
  }
  return groups;
}

console.log(groupBy([1, 2, 3, 3, 4, 4, 4, 5, 5], (x) => x % 2 === 0)); 
// Output: { 'true': [ 2, 4, 4, 4 ], 'false': [ 1, 3, 3, 5, 5 ] }

console.log(groupBy(['a', 'a', 'b', 'b', 'c', 'c'], (x) => x)) 
// Output: { a: [ 'a', 'a' ], b: [ 'b', 'b' ], c: [ 'c', 'c' ] }