Categories
TypeScript

Using TypeScript — Extending 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 extend generic types in TypeScript.

Extending Generic Classes

We can add extra features to existing classes.

For instance, we can write:

class Collection<T extends { name: string }> {
  protected 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];
  }
}

class SearchableCollection<T extends { name: string }> extends Collection<T> {
  find(name: string): T | undefined {
    return this.items.find(item => item.name === name);
  }
}

In the example above, we have the { name: string } type which T must match.

At least we need the name string property.

For instance, we can write:

interface Person {
  name: string;
  age: number;
}

const persons: SearchableCollection<Person> = new SearchableCollection<Person>([
  {
    name: "james",
    age: 1
  }
]);

Person has the name string property and the age number property, but we can still use it.

As long as the object has the name property and its value is a string, we can use it.

Restricting the Generic Type Parameter

We can assign a more restrictive type than the one that’s specified in the type parameter.

For instance, we can write:

class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Owner extends Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    super(name);
    this.age = age;
  }
}

class Collection<T extends Person | Owner> {
  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 use it with a more restrictive type by writing:

const items: Collection<Owner> = new Collection<Owner>([new Owner("james", 1)]);

Owner is more restrictive than Person since it’s a subclass of Person .

Now if we try to add a Person instance into the array we pass into the constructor, we’ll get an error.

For example, if we have:

const items: Collection<Owner> = new Collection<Owner>([
  new Owner("james", 1),
  new Person("jane")
]);

We get the error ‘Property ‘age’ is missing in type ‘Person’ but required in type ‘Owner’.‘

Static Method on a Generic Class

We can define a static method in a generic class.

For instance, we can write:

class Collection<T extends Person | Owner> {
  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);
  }

  static getItem(items: any[], index: number) {
    return items[index];
  }
}

We used the static keyword to indicate that getItem is a static method. We can’t reference any generic type parameters with static methods. The static method is the share across all instances of a class.

We can getItem by writing:

const item: number = Collection.getItem([1, 2, 3], 1);

If we want to add generic type parameters to a static method, then we’ve to add ones that aren’t used by the class.

For example, we can write:

class Collection<T extends Person | Owner> {
  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);
  }

  static getItem<A>(items: A[], index: number) {
    return items[index];
  }
}

We added the A data type placeholder which isn’t used anywhere else.

Then we can call the method by writing:

const item: number = Collection.getItem<number>([1, 2, 3], 1);

Generic Interfaces

We can define generic interfaces.

For instance, we can write:

type Person = {
  name: string;
};

interface Collection<T extends Person> {
  add(...members: T[]): void;
}

Then we can create a class or an object that has the add method that accepts an array with of T type objects that have the members of Person .

Once we have that, we can use the interface as follows:

class DataCollection implements Collection<Person> {
  members: Person[] = [];
  add(...members: Person[]) {
    this.members.push(...members);
  }
}

We set T to Person and then use the Collection interface to enforce our implementation.

Conclusion

We can add generic type parameters to class methods, static methods, and interfaces.

Generic classes can be extended to create subclasses of them, which are also generic.

Likewise, interfaces can also be extended.

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 *