Back to Course

Learn TypeScript

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

Constraining Generics with Type Parameters

In Typescript, it is possible to constrain the types that can be used as arguments for a generic function or class by using type parameters. This can be useful when you want to ensure that a function or class only works with certain types of values, or when you want to access properties or methods of those values that are specific to a certain type. In this article, we will look at how to use type parameters to constrain generics and how to use them effectively in your code.

What are Type Parameters?

Type parameters are special placeholders that you can use in the definition of a generic function or class to specify the types of the arguments or properties that the function or class can take. For example, if you have a generic function that takes an argument of type T, you can specify that T must be a number by using the syntax function myFunction<T extends number>(arg: T). This means that the myFunction function can only be called with an argument of type number, and the type of the arg parameter will be inferred as number.

Type parameters can also be used to specify that a function or class can only work with certain types of values. For example, you might want to create a generic function that only works with arrays of a certain type. In this case, you can specify a type parameter for the array element type, like this: function myFunction<T>(arr: T[]). This specifies that the myFunction function can only be called with an array of values of type T.

Type parameters are very useful for creating flexible and reusable code, but they can also make your code more difficult to read and understand if they are not used correctly. It is important to use type parameters appropriately and only when they are necessary, to ensure that your code is easy to understand and maintain.

Using Type Parameters to Constrain Generics

One of the main benefits of using type parameters is that you can use them to constrain the types that a generic function or class can take. For example, you might want to create a generic function that only works with arrays of numbers, or a class that only works with objects that have a certain property.

To constrain a type parameter, you can use the extends keyword followed by the type that you want to constrain the parameter to. For example, to specify that a type parameter T must be a number, you can use the following syntax: function myFunction<T extends number>(arg: T). This specifies that the myFunction function can only be called with an argument of type number, and the type of the arg parameter will be inferred as number.

You can also use the extends keyword to specify that a type parameter must be a subclass of a certain class or implement a certain interface. For example, to specify that a type parameter T must be a subclass of the Person class, you can use the following syntax: function myFunction<T extends Person>(arg: T). This specifies that the myFunction function can only be called with an argument of type Person or a subclass of Person, and the type of the arg parameter will be inferred as the subclass type.

Using Type Parameters in Function Arguments and Return Types

Type parameters can be used in the arguments and return types of a generic function to specify the types of the values that the function can take and return. For example, you might want to create a generic function that takes an argument of type T and returns a value of type U. In this case, you can specify the type parameters T and U in the function definition, like this: function myFunction<T, U>(arg: T): U. This specifies that the myFunction function can be called with an argument of any type, and it will return a value of any type.

You can also use type parameters to specify that a function can only take certain types of arguments or return certain types of values. For example, you might want to create a generic function that takes an argument of type T and returns a value of type T. In this case, you can specify the same type parameter for both the argument and the return type, like this: function myFunction<T>(arg: T): T. This specifies that the myFunction function can only be called with an argument of the same type as the return value, and the type of both the argument and the return value will be inferred from the type of the argument.

Using Type Parameters in Class Properties and Methods

Type parameters can also be used in the properties and methods of a generic class to specify the types of the values that the class can hold and manipulate. For example, you might want to create a generic class that has a property of type T and a method that takes an argument of type U. In this case, you can specify the type parameters T and U in the class definition, like this:

class MyClass<T, U> {
  property: T;
  method(arg: U): void {
    // code here
  }
}

This specifies that the MyClass class has a property of any type and a method that can be called with an argument of any type. You can use the same type parameter for both the property and the method, or you can use different type parameters for each.

Using Type Parameters Effectively

When using type parameters in your code, it is important to use them appropriately and only when they are necessary. Type parameters can make your code more flexible and reusable, but they can also make your code more difficult to read and understand if they are not used correctly.

Here are a few tips for using type parameters effectively:

  • Use type parameters to specify the types of the arguments and return values of a generic function or the properties and methods of a generic class.
  • Use the extends keyword to constrain type parameters to certain types or subclasses.
  • Use the same type parameter for related values, such as the argument and return value of a function or the property and method of a class.
  • Don’t use type parameters unnecessarily. If a function or class doesn’t need to be generic, don’t use type parameters.
  • Use descriptive names for type parameters to make your code easier to read and understand.

Conclusion

Type parameters are a powerful tool in Typescript for creating flexible and reusable code. By using type parameters to constrain the types that a generic function or class can take, you can create code that is easy to understand and maintain. Remember to use type parameters appropriately and only when they are necessary, and your code will be easier to read and understand.

Exercises

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

Create a generic function mapArray that takes in an array of elements of type T and a function f that takes an element of type T and returns an element of type U. The mapArray function should return a new array of elements of type U that are the result of calling the function f on each element of the input array.

function mapArray<T, U>(array: T[], f: (x: T) => U): U[] {
  let result: U[] = [];
  for (let i = 0; i < array.length; i++) {
    result.push(f(array[i]));
  }
  return result;
}

Create a generic class Pair that has two properties, first and second, both of type T. The Pair class should have a method swap that swaps the values of the first and second properties.

class Pair<T> {
  first: T;
  second: T;

  constructor(first: T, second: T) {
    this.first = first;
    this.second = second;
  }

  swap(): void {
    let temp = this.first;
    this.first = this.second;
    this.second = temp;
  }
}

Create a generic function filterArray that takes in an array of elements of type T and a function predicate that takes an element of type T and returns a boolean. The filterArray function should return a new array of elements of type T that are the elements of the input array for which the predicate function returns true.

function filterArray<T>(array: T[], predicate: (x: T) => boolean): T[] {
  let result: T[] = [];
  for (let i = 0; i < array.length; i++) {
    if (predicate(array[i])) {
      result.push(array[i]);
    }
  }
  return result;
}

Create a generic class Queue that has a property items of type T[]. The Queue class should have the following methods:
(1) enqueue: takes an element of type T and adds it to the end of the items array. (2) dequeue: removes and returns the element at the front of the items array.

class Queue<T> {
  items: T[];

  constructor() {
    this.items = [];
  }

  enqueue(item: T): void {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }
}

Create a generic class Stack that has a property items of type T[]. The Stack class should have the following methods: (1) push: takes an element of type T and adds it to the top of the items (2) pop: removes and returns the element at the top of the items array.

class Stack<T> {
  items: T[];

  constructor() {
    this.items = [];
  }

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}