Categories
TypeScript

Using TypeScript — Generic Types

Spread the love

TypeScript is a natural extension of JavaScript that’s used in many projects in place of JavaScript.

However, not everyone knows how it actually works.

In this article, we’ll look at how to work with generic types in TypeScript.

Generic Types

Generic types allow us to write code that can have different behaviors when we plug in different types to it.

Creating Generic Classes

A generic class is a class that has a generic type parameter. A generic type parameter is a placeholder for a type that’s specified when the class is used to create a new object.

For instance, we can write:

class Collection<T> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

T is the placeholder for a data type.

We can instantiate this class by writing:

const numbers: Collection<number> = new Collection<number>([1, 2, 3]);

We put in the number type in place of T . Then we can add numbers into the items array of our Collection instance.

A generic class can have more than one data type parameter.

Generic Type Arguments

number is the data type argument and Collection is the generic class in the example above.

Different Type Arguments

We can have different data type arguments inserted as a type argument.

For instance, in addition to number , we can put in string instead:

const strings: Collection<string> = new Collection<string>(["foo", "bar"]);

Generic Type Values

We can restrict the type of value in our generic type code by using the extends keyword.

For instance, we can write:

class Collection<T extends number | string> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

Then we can insert the type parameter, which are number , string , or anything narrower.

For instance, we can write:

const numbers: Collection<number> = new Collection<number>([1, 2, 3]);

or:

const strings: Collection<1> = new Collection<1>([1, 1]);

They both work since number and 1 are both subsets of numbers.

extends means that we can assign the subset of one of those types listed.

Constraining Generic Types Using Shape Types

We can also use shape types to restrict generic types.

For instance, we can write:

class Collection<T extends { name: string }> {
  private items: T[] = [];
  constructor(items: T[]) {
    this.items.push(...items);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T {
    return this.items[index];
  }
}

interface Person {
  name: string;
}

const people: Collection<Person> = new Collection<Person>([{ name: "james" }]);

We have:

T extends { name: string }

to restrict our type inside our Collection to be only objects with the name key.

Any other type with the same shape would work.

Multiple Type Parameters

A class can have multiple type parameters.

We can add a second type parameter to our Collection class:

class Collection<T, U> {
  private items: (T | U)[] = [];
  constructor(items: T[], moreItems: U[]) {
    this.items.push(...items, ...moreItems);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T | U {
    return this.items[index];
  }
}

We have the U parameter to add let us add objects of a different type into this.items .

Then we can write:

const items: Collection<number, string> = new Collection<number, string>(
  [1, 2, 3],
  ["foo", "bar"]
);

to create a Collection instance with items that can have numbers or strings.

Additional type parameters are separated with commas like regular functions or method parameters.

Applying Type Parameter to a Method

We can also apply type parameters to a method.

For instance, we can write:

class Collection<T, U> {
  private items: (T | U)[] = [];
  constructor(items: T[], moreItems: U[]) {
    this.items.push(...items, ...moreItems);
  }

  add(items: T) {
    this.items.push(items);
  }

  remove(index: number) {
    this.items.splice(index, 1);
  }

  getItem(index: number): T | U {
    return this.items[index];
  }

  searchItemsByType<U>(searchData: U[], target: U): U[] {
    return searchData.filter(s => s === target);
  }
}

We have getItemsByType which has the searchData parameter with type U[] and target with type U .

Then we can use it by writing:

const results = items.searchItemsByType<string>(["baz", "foo"], "foo");

Then we search the collection of strings for the 'foo' string with getItemsBuType.

Conclusion

We can add generic type parameters to classes to make them work with different types of data.

It can be one or more than one.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *