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.